1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 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'}
67 # Set up the ACL cache on startup
72 RT::ACE->RegisterCacheHandler(sub { RT::Principal->InvalidateACLCache() });
76 Returns true if this principal is a group.
77 Returns undef, otherwise
83 if ( defined $self->PrincipalType &&
84 $self->PrincipalType eq 'Group' ) {
92 Returns true if this principal is a role group.
93 Returns undef, otherwise.
99 return ($self->IsGroup and $self->Object->RoleClass)
105 Returns true if this principal is a User.
106 Returns undef, otherwise
112 if ($self->PrincipalType eq 'User') {
124 Returns the user or group associated with this principal
131 unless ( $self->{'object'} ) {
132 if ( $self->IsUser ) {
133 $self->{'object'} = RT::User->new($self->CurrentUser);
135 elsif ( $self->IsGroup ) {
136 $self->{'object'} = RT::Group->new($self->CurrentUser);
139 $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
142 $self->{'object'}->Load( $self->id );
144 return ($self->{'object'});
151 =head2 GrantRight { Right => RIGHTNAME, Object => undef }
153 A helper function which calls RT::ACE->Create
157 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
170 return (0, "Permission Denied") if $args{'Right'} eq 'ExecuteCode'
171 and RT->Config->Get('DisallowExecuteCode');
173 #ACL check handled in ACE.pm
174 my $ace = RT::ACE->new( $self->CurrentUser );
176 my $type = $self->_GetPrincipalTypeForACL();
178 # If it's a user, we really want to grant the right to their
179 # user equivalence group
180 my ($id, $msg) = $ace->Create(
181 RightName => $args{'Right'},
182 Object => $args{'Object'},
183 PrincipalType => $type,
184 PrincipalId => $self->Id,
191 =head2 RevokeRight { Right => "RightName", Object => "object" }
193 Delete a right that a user has
196 Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's
211 #if we haven't specified any sort of right, we're talking about a global right
212 if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
213 $args{'Object'} = $RT::System;
215 #ACL check handled in ACE.pm
216 my $type = $self->_GetPrincipalTypeForACL();
218 my $ace = RT::ACE->new( $self->CurrentUser );
219 my ($status, $msg) = $ace->LoadByValues(
220 RightName => $args{'Right'},
221 Object => $args{'Object'},
222 PrincipalType => $type,
223 PrincipalId => $self->Id
226 if ( not $status and $msg =~ /Invalid right/ ) {
227 $RT::Logger->warn("Tried to revoke the invalid right '$args{Right}', ignoring it.");
231 return ($status, $msg) unless $status;
233 my $right = $ace->RightName;
234 ($status, $msg) = $ace->Delete;
236 return ($status, $msg);
241 =head2 HasRight (Right => 'right' Object => undef)
243 Checks to see whether this principal has the right "Right" for the Object
244 specified. This takes the params:
254 an RT style object (->id will get its id)
258 Returns 1 if a matching ACE was found. Returns undef if no ACE was found.
260 Use L</HasRights> to fill a fast cache, especially if you're going to
261 check many different rights with the same principal and object.
268 my %args = ( Right => undef,
270 EquivObjects => undef,
274 # RT's SystemUser always has all rights
275 if ( $self->id == RT->SystemUser->id ) {
279 if ( my $right = RT::ACE->CanonicalizeRightName( $args{'Right'} ) ) {
280 $args{'Right'} = $right;
283 "Invalid right. Couldn't canonicalize right '$args{'Right'}'");
287 return undef if $args{'Right'} eq 'ExecuteCode'
288 and RT->Config->Get('DisallowExecuteCode');
290 $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
291 if $args{'EquivObjects'};
293 if ( $self->__Value('Disabled') ) {
294 $RT::Logger->debug( "Disabled User #"
296 . " failed access check for "
301 if ( eval { $args{'Object'}->id } ) {
302 push @{ $args{'EquivObjects'} }, $args{'Object'};
304 $RT::Logger->crit("HasRight called with no valid object");
309 my $cached = $_ACL_CACHE->{
310 $self->id .';:;'. ref($args{'Object'}) .'-'. $args{'Object'}->id
312 return $cached->{'SuperUser'} || $cached->{ $args{'Right'} }
316 unshift @{ $args{'EquivObjects'} },
317 $args{'Object'}->ACLEquivalenceObjects;
318 unshift @{ $args{'EquivObjects'} }, $RT::System;
320 # If we've cached a win or loss for this lookup say so
322 # Construct a hashkeys to cache decisions:
323 # 1) full_hashkey - key for any result and for full combination of uid, right and objects
324 # 2) short_hashkey - one key for each object to store positive results only, it applies
325 # only to direct group rights and partly to role rights
326 my $full_hashkey = join (";:;", $self->id, $args{'Right'});
327 foreach ( @{ $args{'EquivObjects'} } ) {
328 my $ref_id = $self->_ReferenceId($_);
329 $full_hashkey .= ";:;".$ref_id;
331 my $short_hashkey = join(";:;", $self->id, $args{'Right'}, $ref_id);
332 my $cached_answer = $_ACL_CACHE->{ $short_hashkey };
333 return $cached_answer > 0 if defined $cached_answer;
337 my $cached_answer = $_ACL_CACHE->{ $full_hashkey };
338 return $cached_answer > 0 if defined $cached_answer;
341 my ( $hitcount, $via_obj ) = $self->_HasRight(%args);
343 $_ACL_CACHE->{ $full_hashkey } = $hitcount ? 1 : -1;
344 $_ACL_CACHE->{ join ';:;', $self->id, $args{'Right'}, $via_obj } = 1
345 if $via_obj && $hitcount;
352 Returns a hash reference with all rights this principal has on an
353 object. Takes Object as a named argument.
355 Main use case of this method is the following:
357 $ticket->CurrentUser->PrincipalObj->HasRights( Object => $ticket );
359 $ticket->CurrentUserHasRight('A');
361 $ticket->CurrentUserHasRight('Z');
363 Results are cached and the cache is used in this and, as well, in L</HasRight>
364 method speeding it up. Don't use hash reference returned by this method
365 directly for rights checks as it's more complicated then it seems, especially
366 considering config options like 'DisallowExecuteCode'.
374 EquivObjects => undef,
377 return {} if $self->__Value('Disabled');
379 my $object = $args{'Object'};
380 unless ( eval { $object->id } ) {
381 $RT::Logger->crit("HasRights called with no valid object");
384 my $cache_key = $self->id .';:;'. ref($object) .'-'. $object->id;
385 my $cached = $_ACL_CACHE->{ $cache_key };
386 return $cached if $cached;
388 push @{ $args{'EquivObjects'} }, $object;
389 unshift @{ $args{'EquivObjects'} },
390 $args{'Object'}->ACLEquivalenceObjects;
391 unshift @{ $args{'EquivObjects'} }, $RT::System;
396 = "SELECT DISTINCT ACL.RightName "
397 . $self->_HasGroupRightQuery(
398 EquivObjects => $args{'EquivObjects'}
400 my $rights = $RT::Handle->dbh->selectcol_arrayref($query);
402 $RT::Logger->warning( $RT::Handle->dbh->errstr );
405 $res{$_} = 1 foreach @$rights;
410 = "SELECT DISTINCT Groups.Type "
411 . $self->_HasRoleRightQuery(
412 EquivObjects => $args{'EquivObjects'}
414 $roles = $RT::Handle->dbh->selectcol_arrayref($query);
416 $RT::Logger->warning( $RT::Handle->dbh->errstr );
422 = "SELECT DISTINCT ACL.RightName "
423 . $self->_RolesWithRightQuery(
424 EquivObjects => $args{'EquivObjects'}
426 . ' AND ('. join( ' OR ', map "PrincipalType = '$_'", @$roles ) .')'
428 my $rights = $RT::Handle->dbh->selectcol_arrayref($query);
430 $RT::Logger->warning( $RT::Handle->dbh->errstr );
433 $res{$_} = 1 foreach @$rights;
436 delete $res{'ExecuteCode'} if
437 RT->Config->Get('DisallowExecuteCode');
439 $_ACL_CACHE->{ $cache_key } = \%res;
445 Low level HasRight implementation, use HasRight method instead.
452 my ( $hit, @other ) = $self->_HasGroupRight(@_);
453 return ( $hit, @other ) if $hit;
456 my ( $hit, @other ) = $self->_HasRoleRight(@_);
457 return ( $hit, @other ) if $hit;
462 # this method handles role rights partly in situations
463 # where user plays role X on an object and as well the right is
464 # assigned to this role X of the object, for example right CommentOnTicket
465 # is granted to Cc role of a queue and user is in cc list of the queue
468 my %args = ( Right => undef,
474 = "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId "
475 . $self->_HasGroupRightQuery( %args );
477 $self->_Handle->ApplyLimits( \$query, 1 );
478 my ( $hit, $obj, $id ) = $self->_Handle->FetchResult($query);
479 return (0) unless $hit;
481 $obj .= "-$id" if $id;
485 sub _HasGroupRightQuery {
494 = "FROM ACL, Principals, CachedGroupMembers WHERE "
496 # Never find disabled groups.
497 . "Principals.id = ACL.PrincipalId "
498 . "AND Principals.PrincipalType = 'Group' "
499 . "AND Principals.Disabled = 0 "
501 # See if the principal is a member of the group recursively or _is the rightholder_
502 # never find recursively disabled group members
503 # also, check to see if the right is being granted _directly_ to this principal,
504 # as is the case when we want to look up group rights
505 . "AND CachedGroupMembers.GroupId = ACL.PrincipalId "
506 . "AND CachedGroupMembers.GroupId = Principals.id "
507 . "AND CachedGroupMembers.MemberId = ". $self->Id . " "
508 . "AND CachedGroupMembers.Disabled = 0 ";
511 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
512 my $type = ref($obj) || $obj;
513 my $clause = "ACL.ObjectType = '$type'";
515 if ( defined eval { $obj->id } ) { # it might be 0
516 $clause .= " AND ACL.ObjectId = " . $obj->id;
519 push @clauses, "($clause)";
522 $query .= " AND (" . join( ' OR ', @clauses ) . ")";
524 if ( my $right = $args{'Right'} ) {
525 # Only find superuser or rights with the name $right
526 $query .= " AND (ACL.RightName = 'SuperUser' "
527 . ( $right ne 'SuperUser' ? "OR ACL.RightName = '$right'" : '' )
535 my %args = ( Right => undef,
540 my @roles = $self->RolesWithRight(%args);
541 return 0 unless @roles;
543 my $query = "SELECT Groups.id "
544 . $self->_HasRoleRightQuery( %args, Roles => \@roles );
546 $self->_Handle->ApplyLimits( \$query, 1 );
547 my ($hit) = $self->_Handle->FetchResult($query);
553 sub _HasRoleRightQuery {
555 my %args = ( Right => undef,
562 " FROM Groups, Principals, CachedGroupMembers WHERE "
564 # Never find disabled things
565 . "Principals.Disabled = 0 " . "AND CachedGroupMembers.Disabled = 0 "
567 # We always grant rights to Groups
568 . "AND Principals.id = Groups.id "
569 . "AND Principals.PrincipalType = 'Group' "
571 # See if the principal is a member of the group recursively or _is the rightholder_
572 # never find recursively disabled group members
573 # also, check to see if the right is being granted _directly_ to this principal,
574 # as is the case when we want to look up group rights
575 . "AND Principals.id = CachedGroupMembers.GroupId "
576 . "AND CachedGroupMembers.MemberId = " . $self->Id . " "
579 if ( $args{'Roles'} ) {
580 $query .= "AND (" . join( ' OR ',
581 map $RT::Handle->__MakeClauseCaseInsensitive('Groups.Name', '=', "'$_'"),
586 my @object_clauses = RT::Users->_RoleClauses( Groups => @{ $args{'EquivObjects'} } );
587 $query .= " AND (" . join( ' OR ', @object_clauses ) . ")";
591 =head2 RolesWithRight
593 Returns list with names of roles that have right on
594 set of objects. Takes Right, EquiveObjects,
595 IncludeSystemRights and IncludeSuperusers arguments.
597 IncludeSystemRights is true by default, rights
598 granted systemwide are ignored when IncludeSystemRights
599 is set to a false value.
601 IncludeSuperusers is true by default, SuperUser right
602 is not checked if it's set to a false value.
608 my %args = ( Right => undef,
609 IncludeSystemRights => 1,
610 IncludeSuperusers => 1,
615 return () if $args{'Right'} eq 'ExecuteCode'
616 and RT->Config->Get('DisallowExecuteCode');
618 my $query = "SELECT DISTINCT PrincipalType "
619 . $self->_RolesWithRightQuery( %args );
621 my $roles = $RT::Handle->dbh->selectcol_arrayref($query);
623 $RT::Logger->warning( $RT::Handle->dbh->errstr );
629 sub _RolesWithRightQuery {
631 my %args = ( Right => undef,
632 IncludeSystemRights => 1,
633 IncludeSuperusers => 1,
638 my $query = " FROM ACL WHERE"
641 . " PrincipalType != 'Group'";
643 if ( my $right = $args{'Right'} ) {
645 # Only find superuser or rights with the requested right
646 " AND ( RightName = '$right' "
648 # Check SuperUser if we were asked to
649 . ( $args{'IncludeSuperusers'} ? "OR RightName = 'SuperUser' " : '' )
653 # skip rights granted on system level if we were asked to
654 unless ( $args{'IncludeSystemRights'} ) {
655 $query .= " AND ObjectType != 'RT::System'";
658 my (@object_clauses);
659 foreach my $obj ( @{ $args{'EquivObjects'} } ) {
660 my $type = ref($obj) ? ref($obj) : $obj;
662 my $object_clause = "ObjectType = '$type'";
663 if ( my $id = eval { $obj->id } ) {
664 $object_clause .= " AND ObjectId = $id";
666 push @object_clauses, "($object_clause)";
669 # find ACLs that are related to our objects only
670 $query .= " AND (" . join( ' OR ', @object_clauses ) . ")"
677 =head2 InvalidateACLCache
679 Cleans out and reinitializes the user rights cache
683 sub InvalidateACLCache {
691 =head2 _GetPrincipalTypeForACL
693 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
694 return that. if it has no type, return group.
698 sub _GetPrincipalTypeForACL {
700 if ($self->IsRoleGroup) {
701 return $self->Object->Name;
703 return $self->PrincipalType;
711 Returns a list uniquely representing an object or normal scalar.
713 For a scalar, its string value is returned.
714 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 "-".
715 For an object that has an id() method which returns false, its class name is returned.
722 my $id = eval { $scalar->id };
726 return ref($scalar) . "-" . $id;
734 RT->Deprecated( Instead => 'id', Remove => '4.4' );
735 return $self->_Value('ObjectId');
741 if ( exists $args{'ObjectId'} ) {
742 RT->Deprecated( Arguments => 'ObjectId', Instead => 'id', Remove => '4.4' );
744 return $self->SUPER::LoadByCols( %args );
752 Returns the current value of id.
753 (In the database, id is stored as int(11).)
761 Returns the current value of PrincipalType.
762 (In the database, PrincipalType is stored as varchar(16).)
766 =head2 SetPrincipalType VALUE
769 Set PrincipalType to VALUE.
770 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
771 (In the database, PrincipalType will be stored as a varchar(16).)
779 Returns the current value of ObjectId.
780 (In the database, ObjectId is stored as int(11).)
784 =head2 SetObjectId VALUE
787 Set ObjectId to VALUE.
788 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
789 (In the database, ObjectId will be stored as a int(11).)
797 Returns the current value of Disabled.
798 (In the database, Disabled is stored as smallint(6).)
802 =head2 SetDisabled VALUE
805 Set Disabled to VALUE.
806 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
807 (In the database, Disabled will be stored as a smallint(6).)
814 sub _CoreAccessible {
818 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
820 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
822 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
824 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
834 Dependencies => undef,
837 my $deps = $args{'Dependencies'};
841 # Could be wiped allready
842 my $obj = $self->Object;
843 if( defined $obj->id ) {
844 push( @$list, $obj );
847 # Access Control List
848 my $objs = RT::ACL->new( $self->CurrentUser );
850 FIELD => 'PrincipalId',
854 push( @$list, $objs );
856 # AddWatcher/DelWatcher txns
857 foreach my $type ( qw(AddWatcher DelWatcher) ) {
858 my $objs = RT::Transactions->new( $self->CurrentUser );
859 $objs->Limit( FIELD => $type =~ /Add/? 'NewValue': 'OldValue', VALUE => $self->Id );
860 $objs->Limit( FIELD => 'Type', VALUE => $type );
861 push( @$list, $objs );
864 $deps->_PushDependencies(
866 Flags => RT::Shredder::Constants::DEPENDS_ON,
867 TargetObjects => $list,
868 Shredder => $args{'Shredder'}
870 return $self->SUPER::__DependsOn( %args );
873 RT::Base->_ImportOverlays();