3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 # (Except where explictly superceded by other copyright notices)
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
26 no warnings qw(redefine);
27 use vars qw(%_ACL_KEY_CACHE);
36 Returns true if this principal is a group.
37 Returns undef, otherwise
43 if ($self->PrincipalType eq 'Group') {
57 Returns true if this principal is a User.
58 Returns undef, otherwise
64 if ($self->PrincipalType eq 'User') {
78 Returns the user or group associated with this principal
85 unless ($self->{'object'}) {
87 $self->{'object'} = RT::User->new($self->CurrentUser);
89 elsif ($self->IsGroup) {
90 $self->{'object'} = RT::Group->new($self->CurrentUser);
93 $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
96 $self->{'object'}->Load($self->ObjectId());
98 return ($self->{'object'});
104 # {{{ ACL Related routines
108 =head2 GrantRight { Right => RIGHTNAME, Object => undef }
110 A helper function which calls RT::ACE->Create
116 my %args = ( Right => undef,
121 #if we haven't specified any sort of right, we're talking about a global right
122 if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
123 $args{'Object'} = $RT::System;
126 unless ($args{'Right'}) {
127 return(0, $self->loc("Invalid Right"));
131 #ACL check handled in ACE.pm
132 my $ace = RT::ACE->new( $self->CurrentUser );
135 my $type = $self->_GetPrincipalTypeForACL();
137 # If it's a user, we really want to grant the right to their
138 # user equivalence group
139 return ( $ace->Create(RightName => $args{'Right'},
140 Object => $args{'Object'},
141 PrincipalType => $type,
142 PrincipalId => $self->Id
149 =head2 RevokeRight { Right => "RightName", Object => "object" }
151 Delete a right that a user has
164 #if we haven't specified any sort of right, we're talking about a global right
165 if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
166 $args{'Object'} = $RT::System;
168 #ACL check handled in ACE.pm
169 my $type = $self->_GetPrincipalTypeForACL();
171 my $ace = RT::ACE->new( $self->CurrentUser );
173 RightName => $args{'Right'},
174 Object => $args{'Object'},
175 PrincipalType => $type,
176 PrincipalId => $self->Id
179 unless ( $ace->Id ) {
180 return ( 0, $self->loc("ACE not found") );
182 return ( $ace->Delete );
191 =head2 sub HasRight (Right => 'right' Object => undef)
194 Checks to see whether this principal has the right "Right" for the Object
195 specified. If the Object parameter is omitted, checks to see whether the
196 user has the right globally.
198 This still hard codes to check to see if a user has queue-level rights
199 if we ask about a specific ticket.
202 This takes the params:
204 Right => name of a right
208 Object => an RT style object (->id will get its id)
213 Returns 1 if a matching ACE was found.
215 Returns undef if no ACE was found.
222 my %args = ( Right => undef,
224 EquivObjects => undef,
227 if ( $self->Disabled ) {
228 $RT::Logger->err( "Disabled User: " . $self->id . " failed access check for " . $args{'Right'} );
232 if ( !defined $args{'Right'} ) {
234 $RT::Logger->debug( Carp::cluck("HasRight called without a right") );
238 if ( defined( $args{'Object'} )) {
239 return (undef) unless (UNIVERSAL::can( $args{'Object'}, 'id' ) );
240 push(@{$args{'EquivObjects'}}, $args{Object});
242 elsif ( $args{'ObjectId'} && $args{'ObjectType'} ) {
243 $RT::Logger->crit(Carp::cluck("API not supprted"));
246 $RT::Logger->crit("$self HasRight called with no valid object");
250 # If this object is a ticket, we care about ticket roles and queue roles
251 if ( (ref($args{'Object'}) eq 'RT::Ticket') && $args{'Object'}->Id) {
252 # this is a little bit hacky, but basically, now that we've done the ticket roles magic, we load the queue object
253 # and ask all the rest of our questions about the queue.
254 push (@{$args{'EquivObjects'}}, $args{'Object'}->QueueObj);
259 # {{{ If we've cached a win or loss for this lookup say so
261 # {{{ Construct a hashkey to cache decisions in
263 no warnings 'uninitialized';
265 # We don't worry about the hash ordering, as this is only
266 # temporarily used; also if the key changes it would be
267 # invalidated anyway.
269 ";:;", $self->Id, map {
270 $_, # the key of each arguments
271 ($_ eq 'EquivObjects') # for object arrayref...
272 ? map(_ReferenceId($_), @{$args{$_}}) # calculate each
273 : _ReferenceId( $args{$_} ) # otherwise just the value
279 #Anything older than 60 seconds needs to be rechecked
280 my $cache_timeout = ( time - 60 );
282 # {{{ if we've cached a positive result for this query, return 1
283 if ( ( defined $self->_ACLCache->{"$hashkey"} )
284 && ( $self->_ACLCache->{"$hashkey"}{'val'} == 1 )
285 && ( defined $self->_ACLCache->{"$hashkey"}{'set'} )
286 && ( $self->_ACLCache->{"$hashkey"}{'set'} > $cache_timeout ) ) {
288 #$RT::Logger->debug("Cached ACL win for ". $args{'Right'}.$args{'Scope'}. $args{'AppliesTo'}."\n");
293 # {{{ if we've cached a negative result for this query return undef
294 elsif ( ( defined $self->_ACLCache->{"$hashkey"} )
295 && ( $self->_ACLCache->{"$hashkey"}{'val'} == -1 )
296 && ( defined $self->_ACLCache->{"$hashkey"}{'set'} )
297 && ( $self->_ACLCache->{"$hashkey"}{'set'} > $cache_timeout ) ) {
299 #$RT::Logger->debug("Cached ACL loss decision for ". $args{'Right'}.$args{'Scope'}. $args{'AppliesTo'}."\n");
309 # {{{ Out of date docs
311 # We want to grant the right if:
314 # # The user has the right as a member of a system-internal or
315 # # user-defined group
317 # Find all records from the ACL where they're granted to a group
318 # of type "UserDefined" or "System"
319 # for the object "System or the object "Queue N" and the group we're looking
320 # at has the recursive member $self->Id
322 # # The user has the right based on a role
324 # Find all the records from ACL where they're granted to the role "foo"
325 # for the object "System" or the object "Queue N" and the group we're looking
326 # at is of domain ("RT::Queue-Role" and applies to the right queue)
327 # or ("RT::Ticket-Role" and applies to the right ticket)
328 # and the type is the same as the type of the ACL and the group has
329 # the recursive member $self->Id
334 my ( $or_look_at_object_rights, $or_check_roles );
335 my $right = $args{'Right'};
337 # {{{ Construct Right Match
339 # If an object is defined, we want to look at rights for that object
342 push (@look_at_objects, "ACL.ObjectType = 'RT::System'")
343 unless $self->can('_IsOverrideGlobalACL') and $self->_IsOverrideGlobalACL($args{Object});
347 foreach my $obj (@{$args{'EquivObjects'}}) {
348 next unless (UNIVERSAL::can($obj, 'id'));
349 my $type = ref($obj);
351 push @look_at_objects, "(ACL.ObjectType = '$type' AND ACL.ObjectId = '$id')";
357 # {{{ Build that honkin-big SQL query
361 my $query_base = "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE ".
362 # Only find superuser or rights with the name $right
363 "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') ".
364 # Never find disabled groups.
365 "AND Principals.Disabled = 0 " .
366 "AND CachedGroupMembers.Disabled = 0 ".
367 "AND Principals.id = Groups.id " . # We always grant rights to Groups
369 # See if the principal is a member of the group recursively or _is the rightholder_
370 # never find recursively disabled group members
371 # also, check to see if the right is being granted _directly_ to this principal,
372 # as is the case when we want to look up group rights
373 "AND Principals.id = CachedGroupMembers.GroupId AND CachedGroupMembers.MemberId = '" . $self->Id . "' ".
375 # Make sure the rights apply to the entire system or to the object in question
376 "AND ( ".join(' OR ', @look_at_objects).") ";
380 # The groups query does the query based on group membership and individual user rights
382 my $groups_query = $query_base .
384 # limit the result set to groups of types ACLEquivalence (user) UserDefined, SystemInternal and Personal
385 "AND ( ( ACL.PrincipalId = Principals.id AND ACL.PrincipalType = 'Group' AND ".
386 "(Groups.Domain = 'SystemInternal' OR Groups.Domain = 'UserDefined' OR Groups.Domain = 'ACLEquivalence' OR Groups.Domain = 'Personal'))".
391 foreach my $object (@{$args{'EquivObjects'}}) {
392 push (@roles, $self->_RolesForObject(ref($object), $object->id));
395 # The roles query does the query based on roles
398 $roles_query = $query_base . "AND ".
399 " ( (".join (' OR ', @roles)." ) ".
400 " AND Groups.Type = ACL.PrincipalType AND Groups.Id = Principals.id AND Principals.PrincipalType = 'Group') LIMIT 1";
408 # {{{ Actually check the ACL by performing an SQL query
409 # $RT::Logger->debug("Now Trying $groups_query");
410 my $hitcount = $self->_Handle->FetchResult($groups_query);
414 # {{{ if there's a match, the right is granted
417 # Cache a positive hit.
418 $self->_ACLCache->{"$hashkey"}{'set'} = time;
419 $self->_ACLCache->{"$hashkey"}{'val'} = 1;
423 # {{{ If there's no match on groups, try it on roles
426 $hitcount = $self->_Handle->FetchResult($roles_query);
430 # Cache a positive hit.
431 $self->_ACLCache->{"$hashkey"}{'set'} = time;
432 $self->_ACLCache->{"$hashkey"}{'val'} = 1;
437 # cache a negative hit
438 $self->_ACLCache->{"$hashkey"}{'set'} = time;
439 $self->_ACLCache->{"$hashkey"}{'val'} = -1;
449 # {{{ _RolesForObject
453 =head2 _RolesForObject( $object_type, $object_id)
455 Returns an SQL clause finding role groups for Objects
460 sub _RolesForObject {
464 my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = '" . $id. "') ";
479 # Function: _ACLCache
480 # Type : private instance
482 # Lvalue : hash: ACLCache
483 # Desc : Returns a reference to the Key cache hash
488 return(\%_ACL_KEY_CACHE);
493 # {{{ _InvalidateACLCache
495 =head2 _InvalidateACLCache
497 Cleans out and reinitializes the user rights key cache
501 sub _InvalidateACLCache {
502 %_ACL_KEY_CACHE = ();
510 # {{{ _GetPrincipalTypeForACL
512 =head2 _GetPrincipalTypeForACL
514 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type,
515 return that. if it has no type, return group.
519 sub _GetPrincipalTypeForACL {
522 if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
523 $type = $self->Object->Type;
526 $type = $self->PrincipalType;
538 Returns a list uniquely representing an object or normal scalar.
540 For scalars, its string value is returned; for objects that has an
541 id() method, its class name and Id are returned as a string seperated by a "-".
548 # just return the value for non-objects
549 return $scalar unless UNIVERSAL::can($scalar, 'id');
551 # an object -- return the class and id
552 return(ref($scalar)."-". $scalar->id);