1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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} } ] if $args{EquivObjects};
308 if ( $self->Disabled ) {
309 $RT::Logger->error( "Disabled User #"
311 . " failed access check for "
316 if ( defined( $args{'Object'} )
317 && UNIVERSAL::can( $args{'Object'}, 'id' )
318 && $args{'Object'}->id ) {
320 push( @{ $args{'EquivObjects'} }, $args{Object} );
323 $RT::Logger->crit("HasRight called with no valid object");
327 # If this object is a ticket, we care about ticket roles and queue roles
328 if ( UNIVERSAL::isa( $args{'Object'} => 'RT::Ticket' ) ) {
330 # this is a little bit hacky, but basically, now that we've done
331 # the ticket roles magic, we load the queue object
332 # and ask all the rest of our questions about the queue.
333 push( @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj );
337 # {{{ If we've cached a win or loss for this lookup say so
339 # {{{ Construct a hashkey to cache decisions in
341 no warnings 'uninitialized';
343 # We don't worry about the hash ordering, as this is only
344 # temporarily used; also if the key changes it would be
345 # invalidated anyway.
350 $_, # the key of each arguments
351 ( $_ eq 'EquivObjects' ) # for object arrayref...
352 ? map( _ReferenceId($_), @{ $args{$_} } ) # calculate each
353 : _ReferenceId( $args{$_} ) # otherwise just the value
360 # Returns undef on cache miss
361 my $cached_answer = $_ACL_CACHE->fetch($hashkey);
362 if ( defined $cached_answer ) {
363 if ( $cached_answer == 1 ) {
366 elsif ( $cached_answer == -1 ) {
371 my $hitcount = $self->_HasRight( %args );
373 $_ACL_CACHE->set( $hashkey => $hitcount? 1:-1 );
379 Low level HasRight implementation, use HasRight method instead.
393 my $right = $args{'Right'};
394 my @objects = @{ $args{'EquivObjects'} };
396 # If an object is defined, we want to look at rights for that object
398 push( @objects, 'RT::System' )
399 unless $self->can('_IsOverrideGlobalACL')
400 && $self->_IsOverrideGlobalACL( $args{Object} );
402 my ($check_roles, $check_objects) = ('','');
406 foreach my $obj ( @objects ) {
407 my $type = ref($obj)? ref($obj): $obj;
409 $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
411 my $role_clause = "Groups.Domain = '$type-Role'";
412 # XXX: Groups.Instance is VARCHAR in DB, we should quote value
413 # if we want mysql 4.0 use indexes here. we MUST convert that
414 # field to integer and drop this quotes.
415 $role_clause .= " AND Groups.Instance = '$id'" if $id;
416 push @role_clauses, "($role_clause)";
418 my $object_clause = "ACL.ObjectType = '$type'";
419 $object_clause .= " AND ACL.ObjectId = $id" if $id;
420 push @object_clauses, "($object_clause)";
423 $check_roles .= join ' OR ', @role_clauses;
424 $check_objects = join ' OR ', @object_clauses;
428 "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE " .
430 # Only find superuser or rights with the name $right
431 "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
433 # Never find disabled groups.
434 . "AND Principals.Disabled = 0 "
435 . "AND CachedGroupMembers.Disabled = 0 "
437 # We always grant rights to Groups
438 . "AND Principals.id = Groups.id "
439 . "AND Principals.PrincipalType = 'Group' "
441 # See if the principal is a member of the group recursively or _is the rightholder_
442 # never find recursively disabled group members
443 # also, check to see if the right is being granted _directly_ to this principal,
444 # as is the case when we want to look up group rights
445 . "AND Principals.id = CachedGroupMembers.GroupId "
446 . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
448 # Make sure the rights apply to the entire system or to the object in question
449 . "AND ($check_objects) ";
451 # The groups query does the query based on group membership and individual user rights
452 my $groups_query = $query_base
453 # limit the result set to groups of types ACLEquivalence (user),
454 # UserDefined, SystemInternal and Personal. All this we do
455 # via (ACL.PrincipalType = 'Group') condition
456 . "AND ACL.PrincipalId = Principals.id "
457 . "AND ACL.PrincipalType = 'Group' ";
459 $self->_Handle->ApplyLimits( \$groups_query, 1 ); #only return one result
460 my $hitcount = $self->_Handle->FetchResult($groups_query);
461 return 1 if $hitcount; # get out of here if success
463 # The roles query does the query based on roles
464 my $roles_query = $query_base
465 . "AND ACL.PrincipalType = Groups.Type "
466 . "AND ($check_roles) ";
467 $self->_Handle->ApplyLimits( \$roles_query, 1 ); #only return one result
469 $hitcount = $self->_Handle->FetchResult($roles_query);
470 return 1 if $hitcount; # get out of here if success
482 # {{{ InvalidateACLCache
484 =head2 InvalidateACLCache
486 Cleans out and reinitializes the user rights cache
490 sub InvalidateACLCache {
491 $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
492 $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60);
501 # {{{ _GetPrincipalTypeForACL
503 =head2 _GetPrincipalTypeForACL
505 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
506 return that. if it has no type, return group.
510 sub _GetPrincipalTypeForACL {
513 if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
514 $type = $self->Object->Type;
517 $type = $self->PrincipalType;
529 Returns a list uniquely representing an object or normal scalar.
531 For scalars, its string value is returned; for objects that has an
532 id() method, its class name and Id are returned as a string separated by a "-".
539 # just return the value for non-objects
540 return $scalar unless UNIVERSAL::can($scalar, 'id');
542 return ref($scalar) unless $scalar->id;
544 # an object -- return the class and id
545 return(ref($scalar)."-". $scalar->id);