import rt 3.4.4
[freeside.git] / rt / lib / RT / Principal_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
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
16 # from www.gnu.org.
17
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.
22
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., 675 Mass Ave, Cambridge, MA 02139, USA.
26
27
28 # CONTRIBUTION SUBMISSION POLICY:
29
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
35
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
44
45 # END BPS TAGGED BLOCK }}}
46 #
47
48 package RT::Principal;
49
50 use strict;
51 use warnings;
52
53 no warnings qw(redefine);
54
55 use Cache::Simple::TimedExpiry;
56
57
58
59 use RT::Group;
60 use RT::User;
61
62 # Set up the ACL cache on startup
63 our $_ACL_CACHE;
64 InvalidateACLCache();
65
66 # {{{ IsGroup
67
68 =head2 IsGroup
69
70 Returns true if this principal is a group. 
71 Returns undef, otherwise
72
73 =cut
74
75 sub IsGroup {
76     my $self = shift;
77     if ($self->PrincipalType eq 'Group') {
78         return(1);
79     }
80     else {
81         return undef;
82     }
83 }
84
85 # }}}
86
87 # {{{ IsUser
88
89 =head2 IsUser 
90
91 Returns true if this principal is a User. 
92 Returns undef, otherwise
93
94 =cut
95
96 sub IsUser {
97     my $self = shift;
98     if ($self->PrincipalType eq 'User') {
99         return(1);
100     }
101     else {
102         return undef;
103     }
104 }
105
106 # }}}
107
108 # {{{ Object
109
110 =head2 Object
111
112 Returns the user or group associated with this principal
113
114 =cut
115
116 sub Object {
117     my $self = shift;
118
119     unless ($self->{'object'}) {
120     if ($self->IsUser) {
121        $self->{'object'} = RT::User->new($self->CurrentUser);
122     }
123     elsif ($self->IsGroup) {
124         $self->{'object'}  = RT::Group->new($self->CurrentUser);
125     }
126     else { 
127         $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
128         return(undef);
129     }
130     $self->{'object'}->Load($self->ObjectId());
131     }
132     return ($self->{'object'});
133
134
135 }
136 # }}} 
137
138 # {{{ ACL Related routines
139
140 # {{{ GrantRight 
141
142 =head2 GrantRight  { Right => RIGHTNAME, Object => undef }
143
144 A helper function which calls RT::ACE->Create
145
146
147
148    Returns a tuple of (STATUS, MESSAGE);  If the call succeeded, STATUS is true. Otherwise it's 
149    false.
150
151 =cut
152
153 sub GrantRight {
154     my $self = shift;
155     my %args = ( Right => undef,
156                 Object => undef,
157                 @_);
158
159
160     unless ($args{'Right'}) {
161         return(0, $self->loc("Invalid Right"));
162     }
163
164
165     #ACL check handled in ACE.pm
166     my $ace = RT::ACE->new( $self->CurrentUser );
167
168
169     my $type = $self->_GetPrincipalTypeForACL();
170
171     # If it's a user, we really want to grant the right to their 
172     # user equivalence group
173         return ( $ace->Create(RightName => $args{'Right'},
174                           Object => $args{'Object'},
175                           PrincipalType =>  $type,
176                           PrincipalId => $self->Id
177                           ) );
178 }
179 # }}}
180
181 # {{{ RevokeRight
182
183 =head2 RevokeRight { Right => "RightName", Object => "object" }
184
185 Delete a right that a user has 
186
187
188    Returns a tuple of (STATUS, MESSAGE);  If the call succeeded, STATUS is true. Otherwise it's 
189       false.
190
191
192 =cut
193
194 sub RevokeRight {
195
196     my $self = shift;
197     my %args = (
198         Right      => undef,
199         Object => undef,
200         @_
201     );
202
203     #if we haven't specified any sort of right, we're talking about a global right
204     if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
205         $args{'Object'} = $RT::System;
206     }
207     #ACL check handled in ACE.pm
208     my $type = $self->_GetPrincipalTypeForACL();
209
210     my $ace = RT::ACE->new( $self->CurrentUser );
211     $ace->LoadByValues(
212         RightName     => $args{'Right'},
213         Object    => $args{'Object'},
214         PrincipalType => $type,
215         PrincipalId   => $self->Id
216     );
217
218     unless ( $ace->Id ) {
219         return ( 0, $self->loc("ACE not found") );
220     }
221     return ( $ace->Delete );
222 }
223
224 # }}}
225
226 # {{{ sub _CleanupInvalidDelegations
227
228 =head2 sub _CleanupInvalidDelegations { InsideTransaction => undef }
229
230 Revokes all ACE entries delegated by this principal which are
231 inconsistent with this principal's current delegation rights.  Does
232 not perform permission checks, but takes no action and returns success
233 if this principal still retains DelegateRights.  Should only ever be
234 called from inside the RT library.
235
236 If this principal is a group, recursively calls this method on each
237 cached user member of itself.
238
239 If called from inside a transaction, specify a true value for the
240 InsideTransaction parameter.
241
242 Returns a true value if the deletion succeeded; returns a false value
243 and logs an internal error if the deletion fails (should not happen).
244
245 =cut
246
247 # This is currently just a stub for the methods of the same name in
248 # RT::User and RT::Group.
249
250 sub _CleanupInvalidDelegations {
251     my $self = shift;
252     unless ( $self->Id ) {
253         $RT::Logger->warning("Principal not loaded.");
254         return (undef);
255     }
256     return ($self->Object->_CleanupInvalidDelegations(@_));
257 }
258
259 # }}}
260
261 # {{{ sub HasRight
262
263 =head2 sub HasRight (Right => 'right' Object => undef)
264
265
266 Checks to see whether this principal has the right "Right" for the Object
267 specified. If the Object parameter is omitted, checks to see whether the 
268 user has the right globally.
269
270 This still hard codes to check to see if a user has queue-level rights
271 if we ask about a specific ticket.
272
273
274 This takes the params:
275
276     Right => name of a right
277
278     And either:
279
280     Object => an RT style object (->id will get its id)
281
282
283
284
285 Returns 1 if a matching ACE was found.
286
287 Returns undef if no ACE was found.
288
289 =cut
290
291 sub HasRight {
292
293     my $self = shift;
294     my %args = (
295         Right        => undef,
296         Object       => undef,
297         EquivObjects => undef,
298         @_
299     );
300
301     if ( $self->Disabled ) {
302         $RT::Logger->err( "Disabled User:  "
303               . $self->id
304               . " failed access check for "
305               . $args{'Right'} );
306         return (undef);
307     }
308
309     if ( !defined $args{'Right'} ) {
310         $RT::Logger->crit("HasRight called without a right");
311         return (undef);
312     }
313
314     if (   defined( $args{'Object'} )
315         && UNIVERSAL::can( $args{'Object'}, 'id' )
316         && $args{'Object'}->id )
317     {
318         push( @{ $args{'EquivObjects'} }, $args{Object} );
319     }
320     else {
321         $RT::Logger->crit("$self HasRight called with no valid object");
322         return (undef);
323     }
324
325     # If this object is a ticket, we care about ticket roles and queue roles
326     if ( ( ref( $args{'Object'} ) eq 'RT::Ticket' ) && $args{'Object'}->Id ) {
327
328 # this is a little bit hacky, but basically, now that we've done the ticket roles magic, we load the queue object
329 # and ask all the rest of our questions about the queue.
330         push( @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj );
331
332     }
333
334     # {{{ If we've cached a win or loss for this lookup say so
335
336     # {{{ Construct a hashkey to cache decisions in
337     my $hashkey = do {
338         no warnings 'uninitialized';
339
340         # We don't worry about the hash ordering, as this is only
341         # temporarily used; also if the key changes it would be
342         # invalidated anyway.
343         join(
344             ";:;",
345             $self->Id,
346             map {
347                 $_,    # the key of each arguments
348                   ( $_ eq 'EquivObjects' )    # for object arrayref...
349                   ? map( _ReferenceId($_), @{ $args{$_} } )    # calculate each
350                   : _ReferenceId( $args{$_} )    # otherwise just the value
351               } keys %args
352         );
353     };
354
355     # }}}
356
357     # {{{ if we've cached a positive result for this query, return 1
358
359     my $cached_answer = $_ACL_CACHE->fetch($hashkey);
360
361     # Returns undef on cache miss
362     if ( defined $cached_answer ) {
363         if ( $cached_answer == 1 ) {
364             return (1);
365         }
366         elsif ( $cached_answer == -1 ) {
367             return (0);
368         }
369     }
370
371     my ( $or_look_at_object_rights, $or_check_roles );
372     my $right = $args{'Right'};
373
374     # {{{ Construct Right Match
375
376     # If an object is defined, we want to look at rights for that object
377
378     my @look_at_objects;
379     push( @look_at_objects, "ACL.ObjectType = 'RT::System'" )
380       unless $self->can('_IsOverrideGlobalACL')
381       and $self->_IsOverrideGlobalACL( $args{Object} );
382
383     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
384         next unless ( UNIVERSAL::can( $obj, 'id' ) );
385         my $type = ref($obj);
386         my $id   = $obj->id;
387
388         unless ($id) {
389             use Carp;
390             Carp::cluck(
391                 "Trying to check $type rights for an unspecified $type");
392             $RT::Logger->crit(
393                 "Trying to check $type rights for an unspecified $type");
394         }
395         push @look_at_objects,
396           "(ACL.ObjectType = '$type' AND ACL.ObjectId = '$id')";
397     }
398
399     # }}}
400
401     # {{{ Build that honkin-big SQL query
402
403     my $query_base =
404       "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE  " .
405
406       # Only find superuser or rights with the name $right
407       "(ACL.RightName = 'SuperUser' OR  ACL.RightName = '$right') " .
408
409       # Never find disabled groups.
410       "AND Principals.Disabled = 0 "
411       . "AND CachedGroupMembers.Disabled = 0  "
412       . "AND Principals.id = Groups.id "
413       .    # We always grant rights to Groups
414
415 # See if the principal is a member of the group recursively or _is the rightholder_
416 # never find recursively disabled group members
417 # also, check to see if the right is being granted _directly_ to this principal,
418 #  as is the case when we want to look up group rights
419 "AND  Principals.id = CachedGroupMembers.GroupId AND CachedGroupMembers.MemberId = '"
420       . $self->Id . "' "
421       .
422
423   # Make sure the rights apply to the entire system or to the object in question
424       "AND ( " . join( ' OR ', @look_at_objects ) . ") ";
425
426 # The groups query does the query based on group membership and individual user rights
427
428     my $groups_query = $query_base .
429
430 # limit the result set to groups of types ACLEquivalence (user)  UserDefined, SystemInternal and Personal
431 "AND ( (  ACL.PrincipalId = Principals.id AND ACL.PrincipalType = 'Group' AND "
432       . "(Groups.Domain = 'SystemInternal' OR Groups.Domain = 'UserDefined' OR Groups.Domain = 'ACLEquivalence' OR Groups.Domain = 'Personal'))"
433       .
434
435       " ) ";
436     $self->_Handle->ApplyLimits( \$groups_query, 1 );    #only return one result
437
438     my @roles;
439     foreach my $object ( @{ $args{'EquivObjects'} } ) {
440         push( @roles, $self->_RolesForObject( ref($object), $object->id ) );
441     }
442
443     # The roles query does the query based on roles
444     my $roles_query;
445     if (@roles) {
446         $roles_query =
447             $query_base . "AND " . " ( ("
448           . join( ' OR ', @roles ) . " ) "
449           . " AND Groups.Type = ACL.PrincipalType AND Groups.Id = Principals.id AND Principals.PrincipalType = 'Group') ";
450         $self->_Handle->ApplyLimits( \$roles_query, 1 ); #only return one result
451
452     }
453
454     # }}}
455
456     # {{{ Actually check the ACL by performing an SQL query
457     #   $RT::Logger->debug("Now Trying $groups_query");
458     my $hitcount = $self->_Handle->FetchResult($groups_query);
459
460     # }}}
461
462     # {{{ if there's a match, the right is granted
463     if ($hitcount) {
464         $_ACL_CACHE->set( $hashkey => 1 );
465         return (1);
466     }
467
468     # Now check the roles query
469     $hitcount = $self->_Handle->FetchResult($roles_query);
470
471     if ($hitcount) {
472         $_ACL_CACHE->set( $hashkey => 1 );
473         return (1);
474     }
475
476     # We failed to find an acl hit
477     $_ACL_CACHE->set( $hashkey => -1 );
478     return (undef);
479 }
480
481 # }}}
482
483 # {{{ _RolesForObject
484
485
486
487 =head2 _RolesForObject( $object_type, $object_id)
488
489 Returns an SQL clause finding role groups for Objects
490
491 =cut
492
493
494 sub _RolesForObject {
495     my $self = shift;
496     my $type = shift;
497     my $id = shift;
498
499     unless ($id) {
500         $id = '0';
501    }
502
503    # This should never be true.
504    unless ($id =~ /^\d+$/) {
505         $RT::Logger->crit("RT::Prinicipal::_RolesForObject called with type $type and a non-integer id: '$id'");
506         $id = "'$id'";
507    }
508
509     my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = $id) ";
510
511     return($clause);
512 }
513
514 # }}}
515
516 # }}}
517
518 # {{{ ACL caching
519
520
521 # {{{ InvalidateACLCache
522
523 =head2 InvalidateACLCache
524
525 Cleans out and reinitializes the user rights cache
526
527 =cut
528
529 sub InvalidateACLCache {
530     $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
531     $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60);
532
533 }
534
535 # }}}
536
537 # }}}
538
539
540 # {{{ _GetPrincipalTypeForACL
541
542 =head2 _GetPrincipalTypeForACL
543
544 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type, 
545 return that. if it has no type, return group.
546
547 =cut
548
549 sub _GetPrincipalTypeForACL {
550     my $self = shift;
551     my $type;    
552     if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
553         $type = $self->Object->Type;
554     }
555     else {
556         $type = $self->PrincipalType;
557     }
558
559     return($type);
560 }
561
562 # }}}
563
564 # {{{ _ReferenceId
565
566 =head2 _ReferenceId
567
568 Returns a list uniquely representing an object or normal scalar.
569
570 For scalars, its string value is returned; for objects that has an
571 id() method, its class name and Id are returned as a string separated by a "-".
572
573 =cut
574
575 sub _ReferenceId {
576     my $scalar = shift;
577
578     # just return the value for non-objects
579     return $scalar unless UNIVERSAL::can($scalar, 'id');
580
581     # an object -- return the class and id
582     return(ref($scalar)."-". $scalar->id);
583 }
584
585 # }}}
586
587 1;