1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
48 package RT::Principal;
53 no warnings qw(redefine);
55 use Cache::Simple::TimedExpiry;
62 # Set up the ACL cache on startup
70 Returns true if this principal is a group.
71 Returns undef, otherwise
77 if ($self->PrincipalType eq 'Group') {
91 Returns true if this principal is a User.
92 Returns undef, otherwise
98 if ($self->PrincipalType eq 'User') {
112 Returns the user or group associated with this principal
119 unless ($self->{'object'}) {
121 $self->{'object'} = RT::User->new($self->CurrentUser);
123 elsif ($self->IsGroup) {
124 $self->{'object'} = RT::Group->new($self->CurrentUser);
127 $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
130 $self->{'object'}->Load($self->ObjectId());
132 return ($self->{'object'});
138 # {{{ ACL Related routines
142 =head2 GrantRight { Right => RIGHTNAME, Object => undef }
144 A helper function which calls RT::ACE->Create
148 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
155 my %args = ( Right => undef,
160 unless ($args{'Right'}) {
161 return(0, $self->loc("Invalid Right"));
165 #ACL check handled in ACE.pm
166 my $ace = RT::ACE->new( $self->CurrentUser );
169 my $type = $self->_GetPrincipalTypeForACL();
171 # If it's a user, we really want to grant the right to their
172 # user equivalence group
173 return ( $ace->Create(RightName => $args{'Right'},
174 Object => $args{'Object'},
175 PrincipalType => $type,
176 PrincipalId => $self->Id
183 =head2 RevokeRight { Right => "RightName", Object => "object" }
185 Delete a right that a user has
188 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
203 #if we haven't specified any sort of right, we're talking about a global right
204 if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
205 $args{'Object'} = $RT::System;
207 #ACL check handled in ACE.pm
208 my $type = $self->_GetPrincipalTypeForACL();
210 my $ace = RT::ACE->new( $self->CurrentUser );
212 RightName => $args{'Right'},
213 Object => $args{'Object'},
214 PrincipalType => $type,
215 PrincipalId => $self->Id
218 unless ( $ace->Id ) {
219 return ( 0, $self->loc("ACE not found") );
221 return ( $ace->Delete );
226 # {{{ sub _CleanupInvalidDelegations
228 =head2 sub _CleanupInvalidDelegations { InsideTransaction => undef }
230 Revokes all ACE entries delegated by this principal which are
231 inconsistent with this principal's current delegation rights. Does
232 not perform permission checks, but takes no action and returns success
233 if this principal still retains DelegateRights. Should only ever be
234 called from inside the RT library.
236 If this principal is a group, recursively calls this method on each
237 cached user member of itself.
239 If called from inside a transaction, specify a true value for the
240 InsideTransaction parameter.
242 Returns a true value if the deletion succeeded; returns a false value
243 and logs an internal error if the deletion fails (should not happen).
247 # This is currently just a stub for the methods of the same name in
248 # RT::User and RT::Group.
250 sub _CleanupInvalidDelegations {
252 unless ( $self->Id ) {
253 $RT::Logger->warning("Principal not loaded.");
256 return ($self->Object->_CleanupInvalidDelegations(@_));
263 =head2 sub HasRight (Right => 'right' Object => undef)
266 Checks to see whether this principal has the right "Right" for the Object
267 specified. If the Object parameter is omitted, checks to see whether the
268 user has the right globally.
270 This still hard codes to check to see if a user has queue-level rights
271 if we ask about a specific ticket.
274 This takes the params:
276 Right => name of a right
280 Object => an RT style object (->id will get its id)
285 Returns 1 if a matching ACE was found.
287 Returns undef if no ACE was found.
297 EquivObjects => undef,
301 if ( $self->Disabled ) {
302 $RT::Logger->err( "Disabled User: "
304 . " failed access check for "
309 if ( !defined $args{'Right'} ) {
310 $RT::Logger->crit("HasRight called without a right");
314 if ( defined( $args{'Object'} )
315 && UNIVERSAL::can( $args{'Object'}, 'id' )
316 && $args{'Object'}->id )
318 push( @{ $args{'EquivObjects'} }, $args{Object} );
321 $RT::Logger->crit("$self HasRight called with no valid object");
325 # If this object is a ticket, we care about ticket roles and queue roles
326 if ( ( ref( $args{'Object'} ) eq 'RT::Ticket' ) && $args{'Object'}->Id ) {
328 # this is a little bit hacky, but basically, now that we've done the ticket roles magic, we load the queue object
329 # and ask all the rest of our questions about the queue.
330 push( @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj );
334 # {{{ If we've cached a win or loss for this lookup say so
336 # {{{ Construct a hashkey to cache decisions in
338 no warnings 'uninitialized';
340 # We don't worry about the hash ordering, as this is only
341 # temporarily used; also if the key changes it would be
342 # invalidated anyway.
347 $_, # the key of each arguments
348 ( $_ eq 'EquivObjects' ) # for object arrayref...
349 ? map( _ReferenceId($_), @{ $args{$_} } ) # calculate each
350 : _ReferenceId( $args{$_} ) # otherwise just the value
357 # {{{ if we've cached a positive result for this query, return 1
359 my $cached_answer = $_ACL_CACHE->fetch($hashkey);
361 # Returns undef on cache miss
362 if ( defined $cached_answer ) {
363 if ( $cached_answer == 1 ) {
366 elsif ( $cached_answer == -1 ) {
371 my ( $or_look_at_object_rights, $or_check_roles );
372 my $right = $args{'Right'};
374 # {{{ Construct Right Match
376 # If an object is defined, we want to look at rights for that object
379 push( @look_at_objects, "ACL.ObjectType = 'RT::System'" )
380 unless $self->can('_IsOverrideGlobalACL')
381 and $self->_IsOverrideGlobalACL( $args{Object} );
383 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
384 next unless ( UNIVERSAL::can( $obj, 'id' ) );
385 my $type = ref($obj);
391 "Trying to check $type rights for an unspecified $type");
393 "Trying to check $type rights for an unspecified $type");
395 push @look_at_objects,
396 "(ACL.ObjectType = '$type' AND ACL.ObjectId = '$id')";
401 # {{{ Build that honkin-big SQL query
404 "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE " .
406 # Only find superuser or rights with the name $right
407 "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') " .
409 # Never find disabled groups.
410 "AND Principals.Disabled = 0 "
411 . "AND CachedGroupMembers.Disabled = 0 "
412 . "AND Principals.id = Groups.id "
413 . # We always grant rights to Groups
415 # See if the principal is a member of the group recursively or _is the rightholder_
416 # never find recursively disabled group members
417 # also, check to see if the right is being granted _directly_ to this principal,
418 # as is the case when we want to look up group rights
419 "AND Principals.id = CachedGroupMembers.GroupId AND CachedGroupMembers.MemberId = '"
423 # Make sure the rights apply to the entire system or to the object in question
424 "AND ( " . join( ' OR ', @look_at_objects ) . ") ";
426 # The groups query does the query based on group membership and individual user rights
428 my $groups_query = $query_base .
430 # limit the result set to groups of types ACLEquivalence (user) UserDefined, SystemInternal and Personal
431 "AND ( ( ACL.PrincipalId = Principals.id AND ACL.PrincipalType = 'Group' AND "
432 . "(Groups.Domain = 'SystemInternal' OR Groups.Domain = 'UserDefined' OR Groups.Domain = 'ACLEquivalence' OR Groups.Domain = 'Personal'))"
436 $self->_Handle->ApplyLimits( \$groups_query, 1 ); #only return one result
439 foreach my $object ( @{ $args{'EquivObjects'} } ) {
440 push( @roles, $self->_RolesForObject( ref($object), $object->id ) );
443 # The roles query does the query based on roles
447 $query_base . "AND " . " ( ("
448 . join( ' OR ', @roles ) . " ) "
449 . " AND Groups.Type = ACL.PrincipalType AND Groups.Id = Principals.id AND Principals.PrincipalType = 'Group') ";
450 $self->_Handle->ApplyLimits( \$roles_query, 1 ); #only return one result
456 # {{{ Actually check the ACL by performing an SQL query
457 # $RT::Logger->debug("Now Trying $groups_query");
458 my $hitcount = $self->_Handle->FetchResult($groups_query);
462 # {{{ if there's a match, the right is granted
464 $_ACL_CACHE->set( $hashkey => 1 );
468 # Now check the roles query
469 $hitcount = $self->_Handle->FetchResult($roles_query);
472 $_ACL_CACHE->set( $hashkey => 1 );
476 # We failed to find an acl hit
477 $_ACL_CACHE->set( $hashkey => -1 );
483 # {{{ _RolesForObject
487 =head2 _RolesForObject( $object_type, $object_id)
489 Returns an SQL clause finding role groups for Objects
494 sub _RolesForObject {
503 # This should never be true.
504 unless ($id =~ /^\d+$/) {
505 $RT::Logger->crit("RT::Prinicipal::_RolesForObject called with type $type and a non-integer id: '$id'");
509 my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = $id) ";
521 # {{{ InvalidateACLCache
523 =head2 InvalidateACLCache
525 Cleans out and reinitializes the user rights cache
529 sub InvalidateACLCache {
530 $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
531 $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60);
540 # {{{ _GetPrincipalTypeForACL
542 =head2 _GetPrincipalTypeForACL
544 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
545 return that. if it has no type, return group.
549 sub _GetPrincipalTypeForACL {
552 if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
553 $type = $self->Object->Type;
556 $type = $self->PrincipalType;
568 Returns a list uniquely representing an object or normal scalar.
570 For scalars, its string value is returned; for objects that has an
571 id() method, its class name and Id are returned as a string separated by a "-".
578 # just return the value for non-objects
579 return $scalar unless UNIVERSAL::can($scalar, 'id');
581 # an object -- return the class and id
582 return(ref($scalar)."-". $scalar->id);