import of rt 3.0.9
[freeside.git] / rt / lib / RT / Principal_Overlay.pm
1 # BEGIN LICENSE BLOCK
2
3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
4
5 # (Except where explictly superceded by other copyright notices)
6
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
10 # from www.gnu.org.
11
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.
16
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.
21
22
23 # END LICENSE BLOCK
24 use strict;
25
26 no warnings qw(redefine);
27 use vars qw(%_ACL_KEY_CACHE);
28
29 use RT::Group;
30 use RT::User;
31
32 # {{{ IsGroup
33
34 =head2 IsGroup
35
36 Returns true if this principal is a group. 
37 Returns undef, otherwise
38
39 =cut
40
41 sub IsGroup {
42     my $self = shift;
43     if ($self->PrincipalType eq 'Group') {
44         return(1);
45     }
46     else {
47         return undef;
48     }
49 }
50
51 # }}}
52
53 # {{{ IsUser
54
55 =head2 IsUser 
56
57 Returns true if this principal is a User. 
58 Returns undef, otherwise
59
60 =cut
61
62 sub IsUser {
63     my $self = shift;
64     if ($self->PrincipalType eq 'User') {
65         return(1);
66     }
67     else {
68         return undef;
69     }
70 }
71
72 # }}}
73
74 # {{{ Object
75
76 =head2 Object
77
78 Returns the user or group associated with this principal
79
80 =cut
81
82 sub Object {
83     my $self = shift;
84
85     unless ($self->{'object'}) {
86     if ($self->IsUser) {
87        $self->{'object'} = RT::User->new($self->CurrentUser);
88     }
89     elsif ($self->IsGroup) {
90         $self->{'object'}  = RT::Group->new($self->CurrentUser);
91     }
92     else { 
93         $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
94         return(undef);
95     }
96     $self->{'object'}->Load($self->ObjectId());
97     }
98     return ($self->{'object'});
99
100
101 }
102 # }}} 
103
104 # {{{ ACL Related routines
105
106 # {{{ GrantRight 
107
108 =head2 GrantRight  { Right => RIGHTNAME, Object => undef }
109
110 A helper function which calls RT::ACE->Create
111
112 =cut
113
114 sub GrantRight {
115     my $self = shift;
116     my %args = ( Right => undef,
117                 Object => undef,
118                 @_);
119
120
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;
124     }
125
126     unless ($args{'Right'}) {
127         return(0, $self->loc("Invalid Right"));
128     }
129
130
131     #ACL check handled in ACE.pm
132     my $ace = RT::ACE->new( $self->CurrentUser );
133
134
135     my $type = $self->_GetPrincipalTypeForACL();
136
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
143                           ) );
144 }
145 # }}}
146
147 # {{{ RevokeRight
148
149 =head2 RevokeRight { Right => "RightName", Object => "object" }
150
151 Delete a right that a user has 
152
153 =cut
154
155 sub RevokeRight {
156
157     my $self = shift;
158     my %args = (
159         Right      => undef,
160         Object => undef,
161         @_
162     );
163
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;
167     }
168     #ACL check handled in ACE.pm
169     my $type = $self->_GetPrincipalTypeForACL();
170
171     my $ace = RT::ACE->new( $self->CurrentUser );
172     $ace->LoadByValues(
173         RightName     => $args{'Right'},
174         Object    => $args{'Object'},
175         PrincipalType => $type,
176         PrincipalId   => $self->Id
177     );
178
179     unless ( $ace->Id ) {
180         return ( 0, $self->loc("ACE not found") );
181     }
182     return ( $ace->Delete );
183 }
184
185 # }}}
186
187
188
189 # {{{ sub HasRight
190
191 =head2 sub HasRight (Right => 'right' Object => undef)
192
193
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.
197
198 This still hard codes to check to see if a user has queue-level rights
199 if we ask about a specific ticket.
200
201
202 This takes the params:
203
204     Right => name of a right
205
206     And either:
207
208     Object => an RT style object (->id will get its id)
209
210
211
212
213 Returns 1 if a matching ACE was found.
214
215 Returns undef if no ACE was found.
216
217 =cut
218
219 sub HasRight {
220
221     my $self = shift;
222     my %args = ( Right      => undef,
223                  Object     => undef,
224                  EquivObjects    => undef,
225                  @_ );
226
227     if ( $self->Disabled ) {
228         $RT::Logger->err( "Disabled User:  " . $self->id . " failed access check for " . $args{'Right'} );
229         return (undef);
230     }
231
232     if ( !defined $args{'Right'} ) {
233         require Carp;
234         $RT::Logger->debug( Carp::cluck("HasRight called without a right") );
235         return (undef);
236     }
237
238     if ( defined( $args{'Object'} )) {
239         return (undef) unless (UNIVERSAL::can( $args{'Object'}, 'id' ) );
240         push(@{$args{'EquivObjects'}}, $args{Object});
241     }
242     elsif ( $args{'ObjectId'} && $args{'ObjectType'} ) {
243         $RT::Logger->crit(Carp::cluck("API not supprted"));
244     }
245     else {
246         $RT::Logger->crit("$self HasRight called with no valid object");
247         return (undef);
248     }
249
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);
255
256     }
257
258
259     # {{{ If we've cached a win or loss for this lookup say so
260
261     # {{{ Construct a hashkey to cache decisions in
262     my $hashkey = do {
263         no warnings 'uninitialized';
264         
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.
268         join (
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
274             } keys %args
275         );
276     };
277     # }}}
278
279     #Anything older than 60 seconds needs to be rechecked
280     my $cache_timeout = ( time - 60 );
281
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 ) ) {
287
288         #$RT::Logger->debug("Cached ACL win for ".  $args{'Right'}.$args{'Scope'}.  $args{'AppliesTo'}."\n");       
289         return ( 1);
290     }
291     # }}}
292
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 ) ) {
298
299         #$RT::Logger->debug("Cached ACL loss decision for ".  $args{'Right'}.$args{'Scope'}.  $args{'AppliesTo'}."\n");     
300
301         return (undef);
302     }
303     # }}}
304
305     # }}}
306
307
308
309     #  {{{ Out of date docs
310     
311     #   We want to grant the right if:
312
313
314     #    # The user has the right as a member of a system-internal or 
315     #    # user-defined group
316     #
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
321     #
322     #    # The user has the right based on a role
323     #
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
330     #
331
332     # }}}
333
334     my ( $or_look_at_object_rights, $or_check_roles );
335     my $right = $args{'Right'};
336
337     # {{{ Construct Right Match
338
339     # If an object is defined, we want to look at rights for that object
340    
341     my @look_at_objects;
342     push (@look_at_objects, "ACL.ObjectType = 'RT::System'")
343         unless $self->can('_IsOverrideGlobalACL') and $self->_IsOverrideGlobalACL($args{Object});
344
345
346
347     foreach my $obj (@{$args{'EquivObjects'}}) {
348             next unless (UNIVERSAL::can($obj, 'id'));
349             my $type = ref($obj);
350             my $id = $obj->id;
351
352             unless ($id) {
353                 use Carp;
354                 Carp::cluck("Trying to check $type rights for an unspecified $type");
355                 $RT::Logger->crit("Trying to check $type rights for an unspecified $type");
356             }
357             push @look_at_objects, "(ACL.ObjectType = '$type' AND ACL.ObjectId = '$id')"; 
358             }
359
360      
361     # }}}
362
363     # {{{ Build that honkin-big SQL query
364
365     
366
367     my $query_base = "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE  ".
368     # Only find superuser or rights with the name $right
369    "(ACL.RightName = 'SuperUser' OR  ACL.RightName = '$right') ".
370    # Never find disabled groups.
371    "AND Principals.Disabled = 0 " .
372    "AND CachedGroupMembers.Disabled = 0  ".
373     "AND Principals.id = Groups.id " .  # We always grant rights to Groups
374
375     # See if the principal is a member of the group recursively or _is the rightholder_
376     # never find recursively disabled group members
377     # also, check to see if the right is being granted _directly_ to this principal,
378     #  as is the case when we want to look up group rights
379     "AND  Principals.id = CachedGroupMembers.GroupId AND CachedGroupMembers.MemberId = '" . $self->Id . "' ".
380
381     # Make sure the rights apply to the entire system or to the object in question
382     "AND ( ".join(' OR ', @look_at_objects).") ";
383
384
385
386     # The groups query does the query based on group membership and individual user rights
387
388         my $groups_query = $query_base . 
389
390     # limit the result set to groups of types ACLEquivalence (user)  UserDefined, SystemInternal and Personal
391     "AND ( (  ACL.PrincipalId = Principals.id AND ACL.PrincipalType = 'Group' AND ".
392         "(Groups.Domain = 'SystemInternal' OR Groups.Domain = 'UserDefined' OR Groups.Domain = 'ACLEquivalence' OR Groups.Domain = 'Personal'))".
393
394         " ) ";
395         $self->_Handle->ApplyLimits(\$groups_query, 1); #only return one result
396         
397     my @roles;
398     foreach my $object (@{$args{'EquivObjects'}}) { 
399           push (@roles, $self->_RolesForObject(ref($object), $object->id));
400     }
401
402     # The roles query does the query based on roles
403     my $roles_query;
404     if (@roles) {
405          $roles_query = $query_base . "AND ".
406             " ( (".join (' OR ', @roles)." ) ".  
407         " AND Groups.Type = ACL.PrincipalType AND Groups.Id = Principals.id AND Principals.PrincipalType = 'Group') "; 
408         $self->_Handle->ApplyLimits(\$roles_query, 1); #only return one result
409
410    }
411
412
413
414     # }}}
415
416     # {{{ Actually check the ACL by performing an SQL query
417     #   $RT::Logger->debug("Now Trying $groups_query"); 
418     my $hitcount = $self->_Handle->FetchResult($groups_query);
419
420     # }}}
421     
422     # {{{ if there's a match, the right is granted 
423     if ($hitcount) {
424
425         # Cache a positive hit.
426         $self->_ACLCache->{"$hashkey"}{'set'} = time;
427         $self->_ACLCache->{"$hashkey"}{'val'} = 1;
428         return (1);
429     }
430     # }}}
431     # {{{ If there's no match on groups, try it on roles
432     else {   
433
434         $hitcount = $self->_Handle->FetchResult($roles_query);
435
436         if ($hitcount) {
437
438             # Cache a positive hit.
439             $self->_ACLCache->{"$hashkey"}{'set'} = time;
440             $self->_ACLCache->{"$hashkey"}{'val'} = 1;
441             return (1);
442             }
443
444         else {
445             # cache a negative hit
446             $self->_ACLCache->{"$hashkey"}{'set'} = time;
447             $self->_ACLCache->{"$hashkey"}{'val'} = -1;
448
449             return (undef);
450             }
451     }
452     # }}}
453 }
454
455 # }}}
456
457 # {{{ _RolesForObject
458
459
460
461 =head2 _RolesForObject( $object_type, $object_id)
462
463 Returns an SQL clause finding role groups for Objects
464
465 =cut
466
467
468 sub _RolesForObject {
469     my $self = shift;
470     my $type = shift;
471     my $id = shift;
472
473     unless ($id) {
474         $id = '0';
475    }
476
477    # This should never be true.
478    unless ($id =~ /^\d+$/) {
479         $RT::Logger->crit("RT::Prinicipal::_RolesForObject called with type $type and a non-integer id: '$id'");
480         $id = "'$id'";
481    }
482
483     my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = $id) ";
484
485     return($clause);
486 }
487
488 # }}}
489
490 # }}}
491
492 # {{{ ACL caching
493
494 # {{{ _ACLCache
495
496 =head2 _ACLCache
497
498 # Function: _ACLCache
499 # Type    : private instance
500 # Args    : none
501 # Lvalue  : hash: ACLCache
502 # Desc    : Returns a reference to the Key cache hash
503
504 =cut
505
506 sub _ACLCache {
507     return(\%_ACL_KEY_CACHE);
508 }
509
510 # }}}
511
512 # {{{ _InvalidateACLCache
513
514 =head2 _InvalidateACLCache
515
516 Cleans out and reinitializes the user rights key cache
517
518 =cut
519
520 sub _InvalidateACLCache {
521     %_ACL_KEY_CACHE = ();
522 }
523
524 # }}}
525
526 # }}}
527
528
529 # {{{ _GetPrincipalTypeForACL
530
531 =head2 _GetPrincipalTypeForACL
532
533 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type, 
534 return that. if it has no type, return group.
535
536 =cut
537
538 sub _GetPrincipalTypeForACL {
539     my $self = shift;
540     my $type;    
541     if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
542         $type = $self->Object->Type;
543     }
544     else {
545         $type = $self->PrincipalType;
546     }
547
548     return($type);
549 }
550
551 # }}}
552
553 # {{{ _ReferenceId
554
555 =head2 _ReferenceId
556
557 Returns a list uniquely representing an object or normal scalar.
558
559 For scalars, its string value is returned; for objects that has an
560 id() method, its class name and Id are returned as a string seperated by a "-".
561
562 =cut
563
564 sub _ReferenceId {
565     my $scalar = shift;
566
567     # just return the value for non-objects
568     return $scalar unless UNIVERSAL::can($scalar, 'id');
569
570     # an object -- return the class and id
571     return(ref($scalar)."-". $scalar->id);
572 }
573
574 # }}}
575
576 1;