import rt 3.4.6
[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 Returns 1 if a matching ACE was found.
284
285 Returns undef if no ACE was found.
286
287 =cut
288
289 sub HasRight {
290
291     my $self = shift;
292     my %args = (
293         Right        => undef,
294         Object       => undef,
295         EquivObjects => undef,
296         @_,
297     );
298
299     unless ( $args{'Right'} ) {
300         $RT::Logger->crit("HasRight called without a right");
301         return (undef);
302     }
303
304     $args{EquivObjects} = [ @{ $args{EquivObjects} } ] if $args{EquivObjects};
305
306     if ( $self->Disabled ) {
307         $RT::Logger->error( "Disabled User:  "
308               . $self->id
309               . " failed access check for "
310               . $args{'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("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 ( UNIVERSAL::isa( $args{'Object'} => 'RT::Ticket' ) ) {
327
328         # this is a little bit hacky, but basically, now that we've done
329         # the ticket roles magic, we load the queue object
330         # and ask all the rest of our questions about the queue.
331         push( @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj );
332
333     }
334
335     # {{{ If we've cached a win or loss for this lookup say so
336
337     # {{{ Construct a hashkey to cache decisions in
338     my $hashkey = do {
339         no warnings 'uninitialized';
340
341         # We don't worry about the hash ordering, as this is only
342         # temporarily used; also if the key changes it would be
343         # invalidated anyway.
344         join(
345             ";:;",
346             $self->Id,
347             map {
348                 $_,    # the key of each arguments
349                   ( $_ eq 'EquivObjects' )    # for object arrayref...
350                   ? map( _ReferenceId($_), @{ $args{$_} } )    # calculate each
351                   : _ReferenceId( $args{$_} )    # otherwise just the value
352               } keys %args
353         );
354     };
355
356     # }}}
357
358     # Returns undef on cache miss
359     my $cached_answer = $_ACL_CACHE->fetch($hashkey);
360     if ( defined $cached_answer ) {
361         if ( $cached_answer == 1 ) {
362             return (1);
363         }
364         elsif ( $cached_answer == -1 ) {
365             return (undef);
366         }
367     }
368
369     my $hitcount = $self->_HasRight( %args );
370
371     $_ACL_CACHE->set( $hashkey => $hitcount? 1:-1 );
372     return ($hitcount);
373 }
374
375 =head2 _HasRight
376
377 Low level HasRight implementation, use HasRight method instead.
378
379 =cut
380
381 sub _HasRight
382 {
383     my $self = shift;
384     my %args = (
385         Right        => undef,
386         Object       => undef,
387         EquivObjects => [],
388         @_
389     );
390
391     my $right = $args{'Right'};
392     my @objects = @{ $args{'EquivObjects'} };
393
394     # If an object is defined, we want to look at rights for that object
395
396     push( @objects, 'RT::System' )
397       unless $self->can('_IsOverrideGlobalACL')
398              && $self->_IsOverrideGlobalACL( $args{Object} );
399
400     my ($check_roles, $check_objects) = ('','');
401     if( @objects ) {
402         my @role_clauses;
403         my @object_clauses;
404         foreach my $obj ( @objects ) {
405             my $type = ref($obj)? ref($obj): $obj;
406             my $id;
407             $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
408
409             my $role_clause = "Groups.Domain = '$type-Role'";
410             # XXX: Groups.Instance is VARCHAR in DB, we should quote value
411             # if we want mysql 4.0 use indexes here. we MUST convert that
412             # field to integer and drop this quotes.
413             $role_clause   .= " AND Groups.Instance = '$id'" if $id;
414             push @role_clauses, "($role_clause)";
415
416             my $object_clause = "ACL.ObjectType = '$type'";
417             $object_clause   .= " AND ACL.ObjectId = $id" if $id;
418             push @object_clauses, "($object_clause)";
419         }
420
421         $check_roles .= join ' OR ', @role_clauses;
422         $check_objects = join ' OR ', @object_clauses;
423     }
424
425     my $query_base =
426       "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE  " .
427
428       # Only find superuser or rights with the name $right
429       "(ACL.RightName = 'SuperUser' OR  ACL.RightName = '$right') "
430
431       # Never find disabled groups.
432       . "AND Principals.Disabled = 0 "
433       . "AND CachedGroupMembers.Disabled = 0 "
434
435       # We always grant rights to Groups
436       . "AND Principals.id = Groups.id "
437       . "AND Principals.PrincipalType = 'Group' "
438
439       # See if the principal is a member of the group recursively or _is the rightholder_
440       # never find recursively disabled group members
441       # also, check to see if the right is being granted _directly_ to this principal,
442       #  as is the case when we want to look up group rights
443       . "AND Principals.id = CachedGroupMembers.GroupId "
444       . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
445
446       # Make sure the rights apply to the entire system or to the object in question
447       . "AND ($check_objects) ";
448
449     # The groups query does the query based on group membership and individual user rights
450     my $groups_query = $query_base
451       # limit the result set to groups of types ACLEquivalence (user),
452       # UserDefined, SystemInternal and Personal. All this we do
453       # via (ACL.PrincipalType = 'Group') condition
454       . "AND ACL.PrincipalId = Principals.id "
455       . "AND ACL.PrincipalType = 'Group' ";
456
457     $self->_Handle->ApplyLimits( \$groups_query, 1 ); #only return one result
458     my $hitcount = $self->_Handle->FetchResult($groups_query);
459     return 1 if $hitcount; # get out of here if success
460
461     # The roles query does the query based on roles
462     my $roles_query = $query_base
463       . "AND ACL.PrincipalType = Groups.Type "
464       . "AND ($check_roles) ";
465     $self->_Handle->ApplyLimits( \$roles_query, 1 ); #only return one result
466
467     $hitcount = $self->_Handle->FetchResult($roles_query);
468     return 1 if $hitcount; # get out of here if success
469
470     return 0;
471 }
472
473 # }}}
474
475 # }}}
476
477 # {{{ ACL caching
478
479
480 # {{{ InvalidateACLCache
481
482 =head2 InvalidateACLCache
483
484 Cleans out and reinitializes the user rights cache
485
486 =cut
487
488 sub InvalidateACLCache {
489     $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
490     $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60);
491
492 }
493
494 # }}}
495
496 # }}}
497
498
499 # {{{ _GetPrincipalTypeForACL
500
501 =head2 _GetPrincipalTypeForACL
502
503 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type, 
504 return that. if it has no type, return group.
505
506 =cut
507
508 sub _GetPrincipalTypeForACL {
509     my $self = shift;
510     my $type;    
511     if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
512         $type = $self->Object->Type;
513     }
514     else {
515         $type = $self->PrincipalType;
516     }
517
518     return($type);
519 }
520
521 # }}}
522
523 # {{{ _ReferenceId
524
525 =head2 _ReferenceId
526
527 Returns a list uniquely representing an object or normal scalar.
528
529 For scalars, its string value is returned; for objects that has an
530 id() method, its class name and Id are returned as a string separated by a "-".
531
532 =cut
533
534 sub _ReferenceId {
535     my $scalar = shift;
536
537     # just return the value for non-objects
538     return $scalar unless UNIVERSAL::can($scalar, 'id');
539
540     return ref($scalar) unless $scalar->id;
541
542     # an object -- return the class and id
543     return(ref($scalar)."-". $scalar->id);
544 }
545
546 # }}}
547
548 1;