import of rt 3.0.4
[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             push @look_at_objects, "(ACL.ObjectType = '$type' AND ACL.ObjectId = '$id')"; 
352             }
353
354      
355     # }}}
356
357     # {{{ Build that honkin-big SQL query
358
359     
360
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
368
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 . "' ".
374
375     # Make sure the rights apply to the entire system or to the object in question
376     "AND ( ".join(' OR ', @look_at_objects).") ";
377
378
379
380     # The groups query does the query based on group membership and individual user rights
381
382         my $groups_query = $query_base . 
383
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'))".
387
388         " ) LIMIT 1";
389         
390     my @roles;
391     foreach my $object (@{$args{'EquivObjects'}}) { 
392           push (@roles, $self->_RolesForObject(ref($object), $object->id));
393     }
394
395     # The roles query does the query based on roles
396     my $roles_query;
397     if (@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";
401
402    }
403
404
405
406     # }}}
407
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);
411
412     # }}}
413     
414     # {{{ if there's a match, the right is granted 
415     if ($hitcount) {
416
417         # Cache a positive hit.
418         $self->_ACLCache->{"$hashkey"}{'set'} = time;
419         $self->_ACLCache->{"$hashkey"}{'val'} = 1;
420         return (1);
421     }
422     # }}}
423     # {{{ If there's no match on groups, try it on roles
424     else {   
425
426         $hitcount = $self->_Handle->FetchResult($roles_query);
427
428         if ($hitcount) {
429
430             # Cache a positive hit.
431             $self->_ACLCache->{"$hashkey"}{'set'} = time;
432             $self->_ACLCache->{"$hashkey"}{'val'} = 1;
433             return (1);
434             }
435
436         else {
437             # cache a negative hit
438             $self->_ACLCache->{"$hashkey"}{'set'} = time;
439             $self->_ACLCache->{"$hashkey"}{'val'} = -1;
440
441             return (undef);
442             }
443     }
444     # }}}
445 }
446
447 # }}}
448
449 # {{{ _RolesForObject
450
451
452
453 =head2 _RolesForObject( $object_type, $object_id)
454
455 Returns an SQL clause finding role groups for Objects
456
457 =cut
458
459
460 sub _RolesForObject {
461     my $self = shift;
462     my $type = shift;
463     my $id = shift;
464     my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = '" . $id. "') ";
465
466     return($clause);
467 }
468
469 # }}}
470
471 # }}}
472
473 # {{{ ACL caching
474
475 # {{{ _ACLCache
476
477 =head2 _ACLCache
478
479 # Function: _ACLCache
480 # Type    : private instance
481 # Args    : none
482 # Lvalue  : hash: ACLCache
483 # Desc    : Returns a reference to the Key cache hash
484
485 =cut
486
487 sub _ACLCache {
488     return(\%_ACL_KEY_CACHE);
489 }
490
491 # }}}
492
493 # {{{ _InvalidateACLCache
494
495 =head2 _InvalidateACLCache
496
497 Cleans out and reinitializes the user rights key cache
498
499 =cut
500
501 sub _InvalidateACLCache {
502     %_ACL_KEY_CACHE = ();
503 }
504
505 # }}}
506
507 # }}}
508
509
510 # {{{ _GetPrincipalTypeForACL
511
512 =head2 _GetPrincipalTypeForACL
513
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.
516
517 =cut
518
519 sub _GetPrincipalTypeForACL {
520     my $self = shift;
521     my $type;    
522     if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
523         $type = $self->Object->Type;
524     }
525     else {
526         $type = $self->PrincipalType;
527     }
528
529     return($type);
530 }
531
532 # }}}
533
534 # {{{ _ReferenceId
535
536 =head2 _ReferenceId
537
538 Returns a list uniquely representing an object or normal scalar.
539
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 "-".
542
543 =cut
544
545 sub _ReferenceId {
546     my $scalar = shift;
547
548     # just return the value for non-objects
549     return $scalar unless UNIVERSAL::can($scalar, 'id');
550
551     # an object -- return the class and id
552     return(ref($scalar)."-". $scalar->id);
553 }
554
555 # }}}
556
557 1;