1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@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 }}}
51 package RT::Principal;
56 use Cache::Simple::TimedExpiry;
63 # Set up the ACL cache on startup
71 Returns true if this principal is a group.
72 Returns undef, otherwise
78 if ( defined $self->PrincipalType &&
79 $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'} ) {
120 if ( $self->IsUser ) {
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
161 #ACL check handled in ACE.pm
162 my $ace = RT::ACE->new( $self->CurrentUser );
164 my $type = $self->_GetPrincipalTypeForACL();
166 RT->System->QueueCacheNeedsUpdate(1) if $args{'Right'} eq 'SeeQueue';
168 # If it's a user, we really want to grant the right to their
169 # user equivalence group
171 RightName => $args{'Right'},
172 Object => $args{'Object'},
173 PrincipalType => $type,
174 PrincipalId => $self->Id,
181 =head2 RevokeRight { Right => "RightName", Object => "object" }
183 Delete a right that a user has
186 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
201 #if we haven't specified any sort of right, we're talking about a global right
202 if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
203 $args{'Object'} = $RT::System;
205 #ACL check handled in ACE.pm
206 my $type = $self->_GetPrincipalTypeForACL();
208 my $ace = RT::ACE->new( $self->CurrentUser );
209 my ($status, $msg) = $ace->LoadByValues(
210 RightName => $args{'Right'},
211 Object => $args{'Object'},
212 PrincipalType => $type,
213 PrincipalId => $self->Id
216 RT->System->QueueCacheNeedsUpdate(1) if $args{'Right'} eq 'SeeQueue';
217 return ($status, $msg) unless $status;
223 # {{{ sub CleanupInvalidDelegations
225 =head2 sub CleanupInvalidDelegations { InsideTransaction => undef }
227 Revokes all ACE entries delegated by this principal which are
228 inconsistent with this principal's current delegation rights. Does
229 not perform permission checks, but takes no action and returns success
230 if this principal still retains DelegateRights. Should only ever be
231 called from inside the RT library.
233 If this principal is a group, recursively calls this method on each
234 cached user member of itself.
236 If called from inside a transaction, specify a true value for the
237 InsideTransaction parameter.
239 Returns a true value if the deletion succeeded; returns a false value
240 and logs an internal error if the deletion fails (should not happen).
244 # This is currently just a stub for the methods of the same name in
245 # RT::User and RT::Group.
247 # backcompat for 3.8.8 and before
248 *_CleanupInvalidDelegations = \&CleanupInvalidDelegations;
250 sub CleanupInvalidDelegations {
252 unless ( $self->Id ) {
253 $RT::Logger->warning("Principal not loaded.");
256 return ($self->Object->CleanupInvalidDelegations(@_));
264 =head2 sub HasRight (Right => 'right' Object => undef)
267 Checks to see whether this principal has the right "Right" for the Object
268 specified. If the Object parameter is omitted, checks to see whether the
269 user has the right globally.
271 This still hard codes to check to see if a user has queue-level rights
272 if we ask about a specific ticket.
275 This takes the params:
277 Right => name of a right
281 Object => an RT style object (->id will get its id)
284 Returns 1 if a matching ACE was found.
286 Returns undef if no ACE was found.
296 EquivObjects => undef,
300 unless ( $args{'Right'} ) {
301 $RT::Logger->crit("HasRight called without a right");
305 my $canonic_name = RT::ACE->CanonicalizeRightName( $args{'Right'} );
306 unless ( $canonic_name ) {
307 $RT::Logger->error("Invalid right. Couldn't canonicalize right '$args{'Right'}'");
310 $args{'Right'} = $canonic_name;
312 $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
313 if $args{'EquivObjects'};
315 if ( $self->Disabled ) {
316 $RT::Logger->debug( "Disabled User #"
318 . " failed access check for "
323 if ( defined( $args{'Object'} )
324 && UNIVERSAL::can( $args{'Object'}, 'id' )
325 && $args{'Object'}->id ) {
327 push @{ $args{'EquivObjects'} }, $args{'Object'};
330 $RT::Logger->crit("HasRight called with no valid object");
335 unshift @{ $args{'EquivObjects'} }, $args{'Object'}->ACLEquivalenceObjects;
337 unshift @{ $args{'EquivObjects'} }, $RT::System
338 unless $self->can('_IsOverrideGlobalACL')
339 && $self->_IsOverrideGlobalACL( $args{'Object'} );
341 # {{{ If we've cached a win or loss for this lookup say so
343 # Construct a hashkeys to cache decisions:
344 # 1) full_hashkey - key for any result and for full combination of uid, right and objects
345 # 2) short_hashkey - one key for each object to store positive results only, it applies
346 # only to direct group rights and partly to role rights
347 my $self_id = $self->id;
348 my $full_hashkey = join ";:;", $self_id, $args{'Right'};
349 foreach ( @{ $args{'EquivObjects'} } ) {
350 my $ref_id = _ReferenceId($_);
351 $full_hashkey .= ";:;$ref_id";
353 my $short_hashkey = join ";:;", $self_id, $args{'Right'}, $ref_id;
354 my $cached_answer = $_ACL_CACHE->fetch($short_hashkey);
355 return $cached_answer > 0 if defined $cached_answer;
359 my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
360 return $cached_answer > 0 if defined $cached_answer;
364 my ($hitcount, $via_obj) = $self->_HasRight( %args );
366 $_ACL_CACHE->set( $full_hashkey => $hitcount? 1: -1 );
367 $_ACL_CACHE->set( "$self_id;:;$args{'Right'};:;$via_obj" => 1 )
368 if $via_obj && $hitcount;
375 Low level HasRight implementation, use HasRight method instead.
383 my ($hit, @other) = $self->_HasGroupRight( @_ );
384 return ($hit, @other) if $hit;
387 my ($hit, @other) = $self->_HasRoleRight( @_ );
388 return ($hit, @other) if $hit;
393 # this method handles role rights partly in situations
394 # where user plays role X on an object and as well the right is
395 # assigned to this role X of the object, for example right CommentOnTicket
396 # is granted to Cc role of a queue and user is in cc list of the queue
405 my $right = $args{'Right'};
408 "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId " .
409 "FROM ACL, Principals, CachedGroupMembers WHERE " .
411 # Only find superuser or rights with the name $right
412 "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
414 # Never find disabled groups.
415 . "AND Principals.id = ACL.PrincipalId "
416 . "AND Principals.PrincipalType = 'Group' "
417 . "AND Principals.Disabled = 0 "
419 # See if the principal is a member of the group recursively or _is the rightholder_
420 # never find recursively disabled group members
421 # also, check to see if the right is being granted _directly_ to this principal,
422 # as is the case when we want to look up group rights
423 . "AND CachedGroupMembers.GroupId = ACL.PrincipalId "
424 . "AND CachedGroupMembers.GroupId = Principals.id "
425 . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
426 . "AND CachedGroupMembers.Disabled = 0 ";
429 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
430 my $type = ref( $obj ) || $obj;
431 my $clause = "ACL.ObjectType = '$type'";
433 if ( ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id ) {
434 $clause .= " AND ACL.ObjectId = ". $obj->id;
437 push @clauses, "($clause)";
440 $query .= " AND (". join( ' OR ', @clauses ) .")";
443 $self->_Handle->ApplyLimits( \$query, 1 );
444 my ($hit, $obj, $id) = $self->_Handle->FetchResult( $query );
445 return (0) unless $hit;
447 $obj .= "-$id" if $id;
460 my @roles = $self->RolesWithRight( %args );
461 return 0 unless @roles;
463 my $right = $args{'Right'};
467 . "FROM Groups, Principals, CachedGroupMembers WHERE "
469 # Never find disabled things
470 . "Principals.Disabled = 0 "
471 . "AND CachedGroupMembers.Disabled = 0 "
473 # We always grant rights to Groups
474 . "AND Principals.id = Groups.id "
475 . "AND Principals.PrincipalType = 'Group' "
477 # See if the principal is a member of the group recursively or _is the rightholder_
478 # never find recursively disabled group members
479 # also, check to see if the right is being granted _directly_ to this principal,
480 # as is the case when we want to look up group rights
481 . "AND Principals.id = CachedGroupMembers.GroupId "
482 . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
484 . "AND (". join(' OR ', map "Groups.Type = '$_'", @roles ) .")"
487 my (@object_clauses);
488 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
489 my $type = ref($obj)? ref($obj): $obj;
491 $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
493 my $clause = "Groups.Domain = '$type-Role'";
494 # XXX: Groups.Instance is VARCHAR in DB, we should quote value
495 # if we want mysql 4.0 use indexes here. we MUST convert that
496 # field to integer and drop this quotes.
497 $clause .= " AND Groups.Instance = '$id'" if $id;
498 push @object_clauses, "($clause)";
500 $query .= " AND (". join( ' OR ', @object_clauses ) .")";
502 $self->_Handle->ApplyLimits( \$query, 1 );
503 my ($hit) = $self->_Handle->FetchResult( $query );
509 =head2 RolesWithRight
511 Returns list with names of roles that have right on
512 set of objects. Takes Right, EquiveObjects,
513 IncludeSystemRights and IncludeSuperusers arguments.
515 IncludeSystemRights is true by default, rights
516 granted on system level are not accouned when option
517 is set to false value.
519 IncludeSuperusers is true by default, SuperUser right
520 is not checked if it's set to false value.
528 IncludeSystemRights => 1,
529 IncludeSuperusers => 1,
533 my $right = $args{'Right'};
536 "SELECT DISTINCT PrincipalType FROM ACL"
537 # Only find superuser or rights with the name $right
538 ." WHERE ( RightName = '$right' "
539 # Check SuperUser if we were asked to
540 . ($args{'IncludeSuperusers'}? "OR RightName = 'SuperUser' " : '' )
543 ." AND PrincipalType != 'Group'"
546 # skip rights granted on system level if we were asked to
547 unless ( $args{'IncludeSystemRights'} ) {
548 $query .= " AND ObjectType != 'RT::System'";
551 my (@object_clauses);
552 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
553 my $type = ref($obj)? ref($obj): $obj;
555 $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
557 my $object_clause = "ObjectType = '$type'";
558 $object_clause .= " AND ObjectId = $id" if $id;
559 push @object_clauses, "($object_clause)";
561 # find ACLs that are related to our objects only
562 $query .= " AND (". join( ' OR ', @object_clauses ) .")"
565 my $dbh = $RT::Handle->dbh;
566 my $roles = $dbh->selectcol_arrayref($query);
568 $RT::Logger->warning( $dbh->errstr );
581 # {{{ InvalidateACLCache
583 =head2 InvalidateACLCache
585 Cleans out and reinitializes the user rights cache
589 sub InvalidateACLCache {
590 $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
592 $lifetime = $RT::Config->Get('ACLCacheLifetime') if $RT::Config;
593 $_ACL_CACHE->expire_after( $lifetime || 60 );
601 # {{{ _GetPrincipalTypeForACL
603 =head2 _GetPrincipalTypeForACL
605 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
606 return that. if it has no type, return group.
610 sub _GetPrincipalTypeForACL {
613 if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
614 $type = $self->Object->Type;
617 $type = $self->PrincipalType;
629 Returns a list uniquely representing an object or normal scalar.
631 For scalars, its string value is returned; for objects that has an
632 id() method, its class name and Id are returned as a string separated by a "-".
639 # just return the value for non-objects
640 return $scalar unless UNIVERSAL::can($scalar, 'id');
642 return ref($scalar) unless $scalar->id;
644 # an object -- return the class and id
645 return(ref($scalar)."-". $scalar->id);