1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 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;
57 use base 'RT::Record';
59 sub Table {'Principals'}
63 use Cache::Simple::TimedExpiry;
70 # Set up the ACL cache on startup
77 Returns true if this principal is a group.
78 Returns undef, otherwise
84 if ( defined $self->PrincipalType &&
85 $self->PrincipalType eq 'Group' ) {
95 Returns true if this principal is a User.
96 Returns undef, otherwise
102 if ($self->PrincipalType eq 'User') {
114 Returns the user or group associated with this principal
121 unless ( $self->{'object'} ) {
122 if ( $self->IsUser ) {
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'});
141 =head2 GrantRight { Right => RIGHTNAME, Object => undef }
143 A helper function which calls RT::ACE->Create
147 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
160 return (0, "Permission denied") if $args{'Right'} eq 'ExecuteCode'
161 and RT->Config->Get('DisallowExecuteCode');
163 #ACL check handled in ACE.pm
164 my $ace = RT::ACE->new( $self->CurrentUser );
166 my $type = $self->_GetPrincipalTypeForACL();
168 RT->System->QueueCacheNeedsUpdate(1) if $args{'Right'} eq 'SeeQueue';
170 # If it's a user, we really want to grant the right to their
171 # user equivalence group
173 RightName => $args{'Right'},
174 Object => $args{'Object'},
175 PrincipalType => $type,
176 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 if ( not $status and $msg =~ /Invalid right/ ) {
217 $RT::Logger->warn("Tried to revoke the invalid right '$args{Right}', ignoring it.");
221 RT->System->QueueCacheNeedsUpdate(1) if $args{'Right'} eq 'SeeQueue';
222 return ($status, $msg) unless $status;
228 =head2 HasRight (Right => 'right' Object => undef)
230 Checks to see whether this principal has the right "Right" for the Object
231 specified. This takes the params:
241 an RT style object (->id will get its id)
245 Returns 1 if a matching ACE was found. Returns undef if no ACE was found.
247 Use L</HasRights> to fill a fast cache, especially if you're going to
248 check many different rights with the same principal and object.
255 my %args = ( Right => undef,
257 EquivObjects => undef,
261 # RT's SystemUser always has all rights
262 if ( $self->id == RT->SystemUser->id ) {
266 if ( my $right = RT::ACE->CanonicalizeRightName( $args{'Right'} ) ) {
267 $args{'Right'} = $right;
270 "Invalid right. Couldn't canonicalize right '$args{'Right'}'");
274 return undef if $args{'Right'} eq 'ExecuteCode'
275 and RT->Config->Get('DisallowExecuteCode');
277 $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
278 if $args{'EquivObjects'};
280 if ( $self->__Value('Disabled') ) {
281 $RT::Logger->debug( "Disabled User #"
283 . " failed access check for "
288 if ( eval { $args{'Object'}->id } ) {
289 push @{ $args{'EquivObjects'} }, $args{'Object'};
291 $RT::Logger->crit("HasRight called with no valid object");
296 my $cached = $_ACL_CACHE->fetch(
297 $self->id .';:;'. ref($args{'Object'}) .'-'. $args{'Object'}->id
299 return $cached->{'SuperUser'} || $cached->{ $args{'Right'} }
303 unshift @{ $args{'EquivObjects'} },
304 $args{'Object'}->ACLEquivalenceObjects;
306 unshift @{ $args{'EquivObjects'} }, $RT::System
307 unless $self->can('_IsOverrideGlobalACL')
308 && $self->_IsOverrideGlobalACL( $args{'Object'} );
310 # If we've cached a win or loss for this lookup say so
312 # Construct a hashkeys to cache decisions:
313 # 1) full_hashkey - key for any result and for full combination of uid, right and objects
314 # 2) short_hashkey - one key for each object to store positive results only, it applies
315 # only to direct group rights and partly to role rights
316 my $full_hashkey = join (";:;", $self->id, $args{'Right'});
317 foreach ( @{ $args{'EquivObjects'} } ) {
318 my $ref_id = $self->_ReferenceId($_);
319 $full_hashkey .= ";:;".$ref_id;
321 my $short_hashkey = join(";:;", $self->id, $args{'Right'}, $ref_id);
322 my $cached_answer = $_ACL_CACHE->fetch($short_hashkey);
323 return $cached_answer > 0 if defined $cached_answer;
327 my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
328 return $cached_answer > 0 if defined $cached_answer;
331 my ( $hitcount, $via_obj ) = $self->_HasRight(%args);
333 $_ACL_CACHE->set( $full_hashkey => $hitcount ? 1 : -1 );
334 $_ACL_CACHE->set( join(';:;', $self->id, $args{'Right'},$via_obj) => 1 )
335 if $via_obj && $hitcount;
342 Returns a hash reference with all rights this principal has on an
343 object. Takes Object as a named argument.
345 Main use case of this method is the following:
347 $ticket->CurrentUser->PrincipalObj->HasRights( Object => $ticket );
349 $ticket->CurrentUserHasRight('A');
351 $ticket->CurrentUserHasRight('Z');
353 Results are cached and the cache is used in this and, as well, in L</HasRight>
354 method speeding it up. Don't use hash reference returned by this method
355 directly for rights checks as it's more complicated then it seems, especially
356 considering config options like 'DisallowExecuteCode'.
364 EquivObjects => undef,
367 return {} if $self->__Value('Disabled');
369 my $object = $args{'Object'};
370 unless ( eval { $object->id } ) {
371 $RT::Logger->crit("HasRights called with no valid object");
374 my $cache_key = $self->id .';:;'. ref($object) .'-'. $object->id;
375 my $cached = $_ACL_CACHE->fetch($cache_key);
376 return $cached if $cached;
378 push @{ $args{'EquivObjects'} }, $object;
379 unshift @{ $args{'EquivObjects'} },
380 $args{'Object'}->ACLEquivalenceObjects;
381 unshift @{ $args{'EquivObjects'} }, $RT::System
382 unless $self->can('_IsOverrideGlobalACL')
383 && $self->_IsOverrideGlobalACL( $object );
388 = "SELECT DISTINCT ACL.RightName "
389 . $self->_HasGroupRightQuery(
390 EquivObjects => $args{'EquivObjects'}
392 my $rights = $RT::Handle->dbh->selectcol_arrayref($query);
394 $RT::Logger->warning( $RT::Handle->dbh->errstr );
397 $res{$_} = 1 foreach @$rights;
402 = "SELECT DISTINCT Groups.Type "
403 . $self->_HasRoleRightQuery(
404 EquivObjects => $args{'EquivObjects'}
406 $roles = $RT::Handle->dbh->selectcol_arrayref($query);
408 $RT::Logger->warning( $RT::Handle->dbh->errstr );
414 = "SELECT DISTINCT ACL.RightName "
415 . $self->_RolesWithRightQuery(
416 EquivObjects => $args{'EquivObjects'}
418 . ' AND ('. join( ' OR ', map "PrincipalType = '$_'", @$roles ) .')'
420 my $rights = $RT::Handle->dbh->selectcol_arrayref($query);
422 $RT::Logger->warning( $RT::Handle->dbh->errstr );
425 $res{$_} = 1 foreach @$rights;
428 delete $res{'ExecuteCode'} if
429 RT->Config->Get('DisallowExecuteCode');
431 $_ACL_CACHE->store( $cache_key, \%res );
437 Low level HasRight implementation, use HasRight method instead.
444 my ( $hit, @other ) = $self->_HasGroupRight(@_);
445 return ( $hit, @other ) if $hit;
448 my ( $hit, @other ) = $self->_HasRoleRight(@_);
449 return ( $hit, @other ) if $hit;
454 # this method handles role rights partly in situations
455 # where user plays role X on an object and as well the right is
456 # assigned to this role X of the object, for example right CommentOnTicket
457 # is granted to Cc role of a queue and user is in cc list of the queue
460 my %args = ( Right => undef,
466 = "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId "
467 . $self->_HasGroupRightQuery( %args );
469 $self->_Handle->ApplyLimits( \$query, 1 );
470 my ( $hit, $obj, $id ) = $self->_Handle->FetchResult($query);
471 return (0) unless $hit;
473 $obj .= "-$id" if $id;
477 sub _HasGroupRightQuery {
486 = "FROM ACL, Principals, CachedGroupMembers WHERE "
488 # Never find disabled groups.
489 . "Principals.id = ACL.PrincipalId "
490 . "AND Principals.PrincipalType = 'Group' "
491 . "AND Principals.Disabled = 0 "
493 # See if the principal is a member of the group recursively or _is the rightholder_
494 # never find recursively disabled group members
495 # also, check to see if the right is being granted _directly_ to this principal,
496 # as is the case when we want to look up group rights
497 . "AND CachedGroupMembers.GroupId = ACL.PrincipalId "
498 . "AND CachedGroupMembers.GroupId = Principals.id "
499 . "AND CachedGroupMembers.MemberId = ". $self->Id . " "
500 . "AND CachedGroupMembers.Disabled = 0 ";
503 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
504 my $type = ref($obj) || $obj;
505 my $clause = "ACL.ObjectType = '$type'";
507 if ( defined eval { $obj->id } ) { # it might be 0
508 $clause .= " AND ACL.ObjectId = " . $obj->id;
511 push @clauses, "($clause)";
514 $query .= " AND (" . join( ' OR ', @clauses ) . ")";
516 if ( my $right = $args{'Right'} ) {
517 # Only find superuser or rights with the name $right
518 $query .= " AND (ACL.RightName = 'SuperUser' "
519 . ( $right ne 'SuperUser' ? "OR ACL.RightName = '$right'" : '' )
527 my %args = ( Right => undef,
532 my @roles = $self->RolesWithRight(%args);
533 return 0 unless @roles;
535 my $query = "SELECT Groups.id "
536 . $self->_HasRoleRightQuery( %args, Roles => \@roles );
538 $self->_Handle->ApplyLimits( \$query, 1 );
539 my ($hit) = $self->_Handle->FetchResult($query);
545 sub _HasRoleRightQuery {
547 my %args = ( Right => undef,
554 " FROM Groups, Principals, CachedGroupMembers WHERE "
556 # Never find disabled things
557 . "Principals.Disabled = 0 " . "AND CachedGroupMembers.Disabled = 0 "
559 # We always grant rights to Groups
560 . "AND Principals.id = Groups.id "
561 . "AND Principals.PrincipalType = 'Group' "
563 # See if the principal is a member of the group recursively or _is the rightholder_
564 # never find recursively disabled group members
565 # also, check to see if the right is being granted _directly_ to this principal,
566 # as is the case when we want to look up group rights
567 . "AND Principals.id = CachedGroupMembers.GroupId "
568 . "AND CachedGroupMembers.MemberId = " . $self->Id . " "
571 if ( $args{'Roles'} ) {
572 $query .= "AND (" . join( ' OR ', map "Groups.Type = '$_'", @{ $args{'Roles'} } ) . ")";
575 my (@object_clauses);
576 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
577 my $type = ref($obj) ? ref($obj) : $obj;
579 my $clause = "Groups.Domain = '$type-Role'";
581 # XXX: Groups.Instance is VARCHAR in DB, we should quote value
582 # if we want mysql 4.0 use indexes here. we MUST convert that
583 # field to integer and drop this quotes.
584 if ( my $id = eval { $obj->id } ) {
585 $clause .= " AND Groups.Instance = '$id'";
587 push @object_clauses, "($clause)";
589 $query .= " AND (" . join( ' OR ', @object_clauses ) . ")";
593 =head2 RolesWithRight
595 Returns list with names of roles that have right on
596 set of objects. Takes Right, EquiveObjects,
597 IncludeSystemRights and IncludeSuperusers arguments.
599 IncludeSystemRights is true by default, rights
600 granted systemwide are ignored when IncludeSystemRights
601 is set to a false value.
603 IncludeSuperusers is true by default, SuperUser right
604 is not checked if it's set to a false value.
610 my %args = ( Right => undef,
611 IncludeSystemRights => 1,
612 IncludeSuperusers => 1,
617 return () if $args{'Right'} eq 'ExecuteCode'
618 and RT->Config->Get('DisallowExecuteCode');
620 my $query = "SELECT DISTINCT PrincipalType "
621 . $self->_RolesWithRightQuery( %args );
623 my $roles = $RT::Handle->dbh->selectcol_arrayref($query);
625 $RT::Logger->warning( $RT::Handle->dbh->errstr );
631 sub _RolesWithRightQuery {
633 my %args = ( Right => undef,
634 IncludeSystemRights => 1,
635 IncludeSuperusers => 1,
640 my $query = " FROM ACL WHERE"
643 . " PrincipalType != 'Group'";
645 if ( my $right = $args{'Right'} ) {
647 # Only find superuser or rights with the requested right
648 " AND ( RightName = '$right' "
650 # Check SuperUser if we were asked to
651 . ( $args{'IncludeSuperusers'} ? "OR RightName = 'SuperUser' " : '' )
655 # skip rights granted on system level if we were asked to
656 unless ( $args{'IncludeSystemRights'} ) {
657 $query .= " AND ObjectType != 'RT::System'";
660 my (@object_clauses);
661 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
662 my $type = ref($obj) ? ref($obj) : $obj;
664 my $object_clause = "ObjectType = '$type'";
665 if ( my $id = eval { $obj->id } ) {
666 $object_clause .= " AND ObjectId = $id";
668 push @object_clauses, "($object_clause)";
671 # find ACLs that are related to our objects only
672 $query .= " AND (" . join( ' OR ', @object_clauses ) . ")"
679 =head2 InvalidateACLCache
681 Cleans out and reinitializes the user rights cache
685 sub InvalidateACLCache {
686 $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
688 $lifetime = $RT::Config->Get('ACLCacheLifetime') if $RT::Config;
689 $_ACL_CACHE->expire_after( $lifetime || 60 );
696 =head2 _GetPrincipalTypeForACL
698 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
699 return that. if it has no type, return group.
703 sub _GetPrincipalTypeForACL {
705 if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
706 return $self->Object->Type;
708 return $self->PrincipalType;
716 Returns a list uniquely representing an object or normal scalar.
718 For a scalar, its string value is returned.
719 For an object that has an id() method which returns a value, its class name and id are returned as a string separated by a "-".
720 For an object that has an id() method which returns false, its class name is returned.
727 my $id = eval { $scalar->id };
731 return ref($scalar) . "-" . $id;
744 Returns the current value of id.
745 (In the database, id is stored as int(11).)
753 Returns the current value of PrincipalType.
754 (In the database, PrincipalType is stored as varchar(16).)
758 =head2 SetPrincipalType VALUE
761 Set PrincipalType to VALUE.
762 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
763 (In the database, PrincipalType will be stored as a varchar(16).)
771 Returns the current value of ObjectId.
772 (In the database, ObjectId is stored as int(11).)
776 =head2 SetObjectId VALUE
779 Set ObjectId to VALUE.
780 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
781 (In the database, ObjectId will be stored as a int(11).)
789 Returns the current value of Disabled.
790 (In the database, Disabled is stored as smallint(6).)
794 =head2 SetDisabled VALUE
797 Set Disabled to VALUE.
798 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
799 (In the database, Disabled will be stored as a smallint(6).)
806 sub _CoreAccessible {
810 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
812 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
814 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
816 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
821 RT::Base->_ImportOverlays();