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