1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 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., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
50 package RT::Principal;
55 no warnings qw(redefine);
57 use Cache::Simple::TimedExpiry;
64 # Set up the ACL cache on startup
72 Returns true if this principal is a group.
73 Returns undef, otherwise
79 if ($self->PrincipalType eq 'Group') {
93 Returns true if this principal is a User.
94 Returns undef, otherwise
100 if ($self->PrincipalType eq 'User') {
114 Returns the user or group associated with this principal
121 unless ($self->{'object'}) {
123 $self->{'object'} = RT::User->new($self->CurrentUser);
125 elsif ($self->IsGroup) {
126 $self->{'object'} = RT::Group->new($self->CurrentUser);
129 $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
132 $self->{'object'}->Load($self->ObjectId());
134 return ($self->{'object'});
140 # {{{ ACL Related routines
144 =head2 GrantRight { Right => RIGHTNAME, Object => undef }
146 A helper function which calls RT::ACE->Create
150 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
157 my %args = ( Right => undef,
162 unless ($args{'Right'}) {
163 return(0, $self->loc("Invalid Right"));
167 #ACL check handled in ACE.pm
168 my $ace = RT::ACE->new( $self->CurrentUser );
171 my $type = $self->_GetPrincipalTypeForACL();
173 # If it's a user, we really want to grant the right to their
174 # user equivalence group
175 return ( $ace->Create(RightName => $args{'Right'},
176 Object => $args{'Object'},
177 PrincipalType => $type,
178 PrincipalId => $self->Id
185 =head2 RevokeRight { Right => "RightName", Object => "object" }
187 Delete a right that a user has
190 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
205 #if we haven't specified any sort of right, we're talking about a global right
206 if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
207 $args{'Object'} = $RT::System;
209 #ACL check handled in ACE.pm
210 my $type = $self->_GetPrincipalTypeForACL();
212 my $ace = RT::ACE->new( $self->CurrentUser );
214 RightName => $args{'Right'},
215 Object => $args{'Object'},
216 PrincipalType => $type,
217 PrincipalId => $self->Id
220 unless ( $ace->Id ) {
221 return ( 0, $self->loc("ACE not found") );
223 return ( $ace->Delete );
228 # {{{ sub _CleanupInvalidDelegations
230 =head2 sub _CleanupInvalidDelegations { InsideTransaction => undef }
232 Revokes all ACE entries delegated by this principal which are
233 inconsistent with this principal's current delegation rights. Does
234 not perform permission checks, but takes no action and returns success
235 if this principal still retains DelegateRights. Should only ever be
236 called from inside the RT library.
238 If this principal is a group, recursively calls this method on each
239 cached user member of itself.
241 If called from inside a transaction, specify a true value for the
242 InsideTransaction parameter.
244 Returns a true value if the deletion succeeded; returns a false value
245 and logs an internal error if the deletion fails (should not happen).
249 # This is currently just a stub for the methods of the same name in
250 # RT::User and RT::Group.
252 sub _CleanupInvalidDelegations {
254 unless ( $self->Id ) {
255 $RT::Logger->warning("Principal not loaded.");
258 return ($self->Object->_CleanupInvalidDelegations(@_));
265 =head2 sub HasRight (Right => 'right' Object => undef)
268 Checks to see whether this principal has the right "Right" for the Object
269 specified. If the Object parameter is omitted, checks to see whether the
270 user has the right globally.
272 This still hard codes to check to see if a user has queue-level rights
273 if we ask about a specific ticket.
276 This takes the params:
278 Right => name of a right
282 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 unless ( $args{'Right'} ) {
302 $RT::Logger->crit("HasRight called without a right");
306 $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
307 if $args{'EquivObjects'};
309 if ( $self->Disabled ) {
310 $RT::Logger->error( "Disabled User #"
312 . " failed access check for "
317 if ( defined( $args{'Object'} )
318 && UNIVERSAL::can( $args{'Object'}, 'id' )
319 && $args{'Object'}->id ) {
321 push @{ $args{'EquivObjects'} }, $args{'Object'};
324 $RT::Logger->crit("HasRight called with no valid object");
328 # If this object is a ticket, we care about ticket roles and queue roles
329 if ( UNIVERSAL::isa( $args{'Object'} => 'RT::Ticket' ) ) {
331 # this is a little bit hacky, but basically, now that we've done
332 # the ticket roles magic, we load the queue object
333 # and ask all the rest of our questions about the queue.
334 unshift @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj;
338 unshift @{ $args{'EquivObjects'} }, $RT::System
339 unless $self->can('_IsOverrideGlobalACL')
340 && $self->_IsOverrideGlobalACL( $args{'Object'} );
343 # {{{ If we've cached a win or loss for this lookup say so
345 # Construct a hashkeys to cache decisions:
346 # 1) full_hashkey - key for any result and for full combination of uid, right and objects
347 # 2) short_hashkey - one key for each object to store positive results only, it applies
348 # only to direct group rights and partly to role rights
349 my $self_id = $self->id;
350 my $full_hashkey = join ";:;", $self_id, $args{'Right'};
351 foreach ( @{ $args{'EquivObjects'} } ) {
352 my $ref_id = _ReferenceId($_);
353 $full_hashkey .= ";:;$ref_id";
355 my $short_hashkey = join ";:;", $self_id, $args{'Right'}, $ref_id;
356 my $cached_answer = $_ACL_CACHE->fetch($short_hashkey);
357 return $cached_answer > 0 if defined $cached_answer;
361 my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
362 return $cached_answer > 0 if defined $cached_answer;
366 my ($hitcount, $via_obj) = $self->_HasRight( %args );
368 $_ACL_CACHE->set( $full_hashkey => $hitcount? 1: -1 );
369 $_ACL_CACHE->set( "$self_id;:;$args{'Right'};:;$via_obj" => 1 )
370 if $via_obj && $hitcount;
377 Low level HasRight implementation, use HasRight method instead.
385 my ($hit, @other) = $self->_HasGroupRight( @_ );
386 return ($hit, @other) if $hit;
389 my ($hit, @other) = $self->_HasRoleRight( @_ );
390 return ($hit, @other) if $hit;
395 # this method handles role rights partly in situations
396 # where user plays role X on an object and as well the right is
397 # assigned to this role X of the object, for example right CommentOnTicket
398 # is granted to Cc role of a queue and user is in cc list of the queue
407 my $right = $args{'Right'};
410 "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId " .
411 "FROM ACL, Principals, CachedGroupMembers WHERE " .
413 # Only find superuser or rights with the name $right
414 "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
416 # Never find disabled groups.
417 . "AND Principals.id = ACL.PrincipalId "
418 . "AND Principals.PrincipalType = 'Group' "
419 . "AND Principals.Disabled = 0 "
421 # See if the principal is a member of the group recursively or _is the rightholder_
422 # never find recursively disabled group members
423 # also, check to see if the right is being granted _directly_ to this principal,
424 # as is the case when we want to look up group rights
425 . "AND CachedGroupMembers.GroupId = ACL.PrincipalId "
426 . "AND CachedGroupMembers.GroupId = Principals.id "
427 . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
428 . "AND CachedGroupMembers.Disabled = 0 ";
431 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
432 my $type = ref( $obj ) || $obj;
433 my $clause = "ACL.ObjectType = '$type'";
435 if ( ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id ) {
436 $clause .= " AND ACL.ObjectId = ". $obj->id;
439 push @clauses, "($clause)";
442 $query .= " AND (". join( ' OR ', @clauses ) .")";
445 $self->_Handle->ApplyLimits( \$query, 1 );
446 my ($hit, $obj, $id) = $self->_Handle->FetchResult( $query );
447 return (0) unless $hit;
449 $obj .= "-$id" if $id;
461 my $right = $args{'Right'};
465 "FROM ACL, Groups, Principals, CachedGroupMembers WHERE " .
467 # Only find superuser or rights with the name $right
468 "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
470 # Never find disabled things
471 . "AND Principals.Disabled = 0 "
472 . "AND CachedGroupMembers.Disabled = 0 "
474 # We always grant rights to Groups
475 . "AND Principals.id = Groups.id "
476 . "AND Principals.PrincipalType = 'Group' "
478 # See if the principal is a member of the group recursively or _is the rightholder_
479 # never find recursively disabled group members
480 # also, check to see if the right is being granted _directly_ to this principal,
481 # as is the case when we want to look up group rights
482 . "AND Principals.id = CachedGroupMembers.GroupId "
483 . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
484 . "AND ACL.PrincipalType = Groups.Type ";
486 my (@object_clauses);
487 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
488 my $type = ref($obj)? ref($obj): $obj;
490 $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
492 my $object_clause = "ACL.ObjectType = '$type'";
493 $object_clause .= " AND ACL.ObjectId = $id" if $id;
494 push @object_clauses, "($object_clause)";
496 # find ACLs that are related to our objects only
497 $query .= " AND (". join( ' OR ', @object_clauses ) .")";
499 # because of mysql bug in versions up to 5.0.45 we do one query per object
500 # each query should be faster on any DB as it uses indexes more effective
501 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
502 my $type = ref($obj)? ref($obj): $obj;
504 $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
507 $tmp .= " AND Groups.Domain = '$type-Role'";
508 # XXX: Groups.Instance is VARCHAR in DB, we should quote value
509 # if we want mysql 4.0 use indexes here. we MUST convert that
510 # field to integer and drop this quotes.
511 $tmp .= " AND Groups.Instance = '$id'" if $id;
513 $self->_Handle->ApplyLimits( \$tmp, 1 );
514 my ($hit) = $self->_Handle->FetchResult( $tmp );
528 # {{{ InvalidateACLCache
530 =head2 InvalidateACLCache
532 Cleans out and reinitializes the user rights cache
536 sub InvalidateACLCache {
537 $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
538 $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60);
547 # {{{ _GetPrincipalTypeForACL
549 =head2 _GetPrincipalTypeForACL
551 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
552 return that. if it has no type, return group.
556 sub _GetPrincipalTypeForACL {
559 if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
560 $type = $self->Object->Type;
563 $type = $self->PrincipalType;
575 Returns a list uniquely representing an object or normal scalar.
577 For scalars, its string value is returned; for objects that has an
578 id() method, its class name and Id are returned as a string separated by a "-".
585 # just return the value for non-objects
586 return $scalar unless UNIVERSAL::can($scalar, 'id');
588 return ref($scalar) unless $scalar->id;
590 # an object -- return the class and id
591 return(ref($scalar)."-". $scalar->id);