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)
283 Returns 1 if a matching ACE was found.
285 Returns undef if no ACE was found.
295 EquivObjects => undef,
299 unless ( $args{'Right'} ) {
300 $RT::Logger->crit("HasRight called without a right");
304 $args{EquivObjects} = [ @{ $args{EquivObjects} } ] if $args{EquivObjects};
306 if ( $self->Disabled ) {
307 $RT::Logger->error( "Disabled User: "
309 . " failed access check for "
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("HasRight called with no valid object");
325 # If this object is a ticket, we care about ticket roles and queue roles
326 if ( UNIVERSAL::isa( $args{'Object'} => 'RT::Ticket' ) ) {
328 # this is a little bit hacky, but basically, now that we've done
329 # the ticket roles magic, we load the queue object
330 # and ask all the rest of our questions about the queue.
331 push( @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj );
335 # {{{ If we've cached a win or loss for this lookup say so
337 # {{{ Construct a hashkey to cache decisions in
339 no warnings 'uninitialized';
341 # We don't worry about the hash ordering, as this is only
342 # temporarily used; also if the key changes it would be
343 # invalidated anyway.
348 $_, # the key of each arguments
349 ( $_ eq 'EquivObjects' ) # for object arrayref...
350 ? map( _ReferenceId($_), @{ $args{$_} } ) # calculate each
351 : _ReferenceId( $args{$_} ) # otherwise just the value
358 # Returns undef on cache miss
359 my $cached_answer = $_ACL_CACHE->fetch($hashkey);
360 if ( defined $cached_answer ) {
361 if ( $cached_answer == 1 ) {
364 elsif ( $cached_answer == -1 ) {
369 my $hitcount = $self->_HasRight( %args );
371 $_ACL_CACHE->set( $hashkey => $hitcount? 1:-1 );
377 Low level HasRight implementation, use HasRight method instead.
391 my $right = $args{'Right'};
392 my @objects = @{ $args{'EquivObjects'} };
394 # If an object is defined, we want to look at rights for that object
396 push( @objects, 'RT::System' )
397 unless $self->can('_IsOverrideGlobalACL')
398 && $self->_IsOverrideGlobalACL( $args{Object} );
400 my ($check_roles, $check_objects) = ('','');
404 foreach my $obj ( @objects ) {
405 my $type = ref($obj)? ref($obj): $obj;
407 $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
409 my $role_clause = "Groups.Domain = '$type-Role'";
410 # XXX: Groups.Instance is VARCHAR in DB, we should quote value
411 # if we want mysql 4.0 use indexes here. we MUST convert that
412 # field to integer and drop this quotes.
413 $role_clause .= " AND Groups.Instance = '$id'" if $id;
414 push @role_clauses, "($role_clause)";
416 my $object_clause = "ACL.ObjectType = '$type'";
417 $object_clause .= " AND ACL.ObjectId = $id" if $id;
418 push @object_clauses, "($object_clause)";
421 $check_roles .= join ' OR ', @role_clauses;
422 $check_objects = join ' OR ', @object_clauses;
426 "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE " .
428 # Only find superuser or rights with the name $right
429 "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
431 # Never find disabled groups.
432 . "AND Principals.Disabled = 0 "
433 . "AND CachedGroupMembers.Disabled = 0 "
435 # We always grant rights to Groups
436 . "AND Principals.id = Groups.id "
437 . "AND Principals.PrincipalType = 'Group' "
439 # See if the principal is a member of the group recursively or _is the rightholder_
440 # never find recursively disabled group members
441 # also, check to see if the right is being granted _directly_ to this principal,
442 # as is the case when we want to look up group rights
443 . "AND Principals.id = CachedGroupMembers.GroupId "
444 . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
446 # Make sure the rights apply to the entire system or to the object in question
447 . "AND ($check_objects) ";
449 # The groups query does the query based on group membership and individual user rights
450 my $groups_query = $query_base
451 # limit the result set to groups of types ACLEquivalence (user),
452 # UserDefined, SystemInternal and Personal. All this we do
453 # via (ACL.PrincipalType = 'Group') condition
454 . "AND ACL.PrincipalId = Principals.id "
455 . "AND ACL.PrincipalType = 'Group' ";
457 $self->_Handle->ApplyLimits( \$groups_query, 1 ); #only return one result
458 my $hitcount = $self->_Handle->FetchResult($groups_query);
459 return 1 if $hitcount; # get out of here if success
461 # The roles query does the query based on roles
462 my $roles_query = $query_base
463 . "AND ACL.PrincipalType = Groups.Type "
464 . "AND ($check_roles) ";
465 $self->_Handle->ApplyLimits( \$roles_query, 1 ); #only return one result
467 $hitcount = $self->_Handle->FetchResult($roles_query);
468 return 1 if $hitcount; # get out of here if success
480 # {{{ InvalidateACLCache
482 =head2 InvalidateACLCache
484 Cleans out and reinitializes the user rights cache
488 sub InvalidateACLCache {
489 $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
490 $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60);
499 # {{{ _GetPrincipalTypeForACL
501 =head2 _GetPrincipalTypeForACL
503 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
504 return that. if it has no type, return group.
508 sub _GetPrincipalTypeForACL {
511 if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
512 $type = $self->Object->Type;
515 $type = $self->PrincipalType;
527 Returns a list uniquely representing an object or normal scalar.
529 For scalars, its string value is returned; for objects that has an
530 id() method, its class name and Id are returned as a string separated by a "-".
537 # just return the value for non-objects
538 return $scalar unless UNIVERSAL::can($scalar, 'id');
540 return ref($scalar) unless $scalar->id;
542 # an object -- return the class and id
543 return(ref($scalar)."-". $scalar->id);