import rt 3.6.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-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'} } ]
307         if $args{'EquivObjects'};
308
309     if ( $self->Disabled ) {
310         $RT::Logger->error( "Disabled User #"
311               . $self->id
312               . " failed access check for "
313               . $args{'Right'} );
314         return (undef);
315     }
316
317     if (   defined( $args{'Object'} )
318         && UNIVERSAL::can( $args{'Object'}, 'id' )
319         && $args{'Object'}->id ) {
320
321         push @{ $args{'EquivObjects'} }, $args{'Object'};
322     }
323     else {
324         $RT::Logger->crit("HasRight called with no valid object");
325         return (undef);
326     }
327
328     # If this object is a ticket, we care about ticket roles and queue roles
329     if ( UNIVERSAL::isa( $args{'Object'} => 'RT::Ticket' ) ) {
330
331         # this is a little bit hacky, but basically, now that we've done
332         # the ticket roles magic, we load the queue object
333         # and ask all the rest of our questions about the queue.
334         unshift @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj;
335
336     }
337
338     unshift @{ $args{'EquivObjects'} }, $RT::System
339         unless $self->can('_IsOverrideGlobalACL')
340                && $self->_IsOverrideGlobalACL( $args{'Object'} );
341
342
343     # {{{ If we've cached a win or loss for this lookup say so
344
345     # Construct a hashkeys to cache decisions:
346     # 1) full_hashkey - key for any result and for full combination of uid, right and objects
347     # 2) short_hashkey - one key for each object to store positive results only, it applies
348     # only to direct group rights and partly to role rights
349     my $self_id = $self->id;
350     my $full_hashkey = join ";:;", $self_id, $args{'Right'};
351     foreach ( @{ $args{'EquivObjects'} } ) {
352         my $ref_id = _ReferenceId($_);
353         $full_hashkey .= ";:;$ref_id";
354
355         my $short_hashkey = join ";:;", $self_id, $args{'Right'}, $ref_id;
356         my $cached_answer = $_ACL_CACHE->fetch($short_hashkey);
357         return $cached_answer > 0 if defined $cached_answer;
358     }
359
360     {
361         my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
362         return $cached_answer > 0 if defined $cached_answer;
363     }
364
365
366     my ($hitcount, $via_obj) = $self->_HasRight( %args );
367
368     $_ACL_CACHE->set( $full_hashkey => $hitcount? 1: -1 );
369     $_ACL_CACHE->set( "$self_id;:;$args{'Right'};:;$via_obj" => 1 )
370         if $via_obj && $hitcount;
371
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     {
385         my ($hit, @other) = $self->_HasGroupRight( @_ );
386         return ($hit, @other) if $hit;
387     }
388     {
389         my ($hit, @other) = $self->_HasRoleRight( @_ );
390         return ($hit, @other) if $hit;
391     }
392     return (0);
393 }
394
395 # this method handles role rights partly in situations
396 # where user plays role X on an object and as well the right is
397 # assigned to this role X of the object, for example right CommentOnTicket
398 # is granted to Cc role of a queue and user is in cc list of the queue
399 sub _HasGroupRight
400 {
401     my $self = shift;
402     my %args = (
403         Right        => undef,
404         EquivObjects => [],
405         @_
406     );
407     my $right = $args{'Right'};
408
409     my $query =
410       "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId " .
411       "FROM ACL, Principals, CachedGroupMembers WHERE " .
412
413       # Only find superuser or rights with the name $right
414       "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
415
416       # Never find disabled groups.
417       . "AND Principals.id = ACL.PrincipalId "
418       . "AND Principals.PrincipalType = 'Group' "
419       . "AND Principals.Disabled = 0 "
420
421       # See if the principal is a member of the group recursively or _is the rightholder_
422       # never find recursively disabled group members
423       # also, check to see if the right is being granted _directly_ to this principal,
424       #  as is the case when we want to look up group rights
425       . "AND CachedGroupMembers.GroupId  = ACL.PrincipalId "
426       . "AND CachedGroupMembers.GroupId  = Principals.id "
427       . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
428       . "AND CachedGroupMembers.Disabled = 0 ";
429
430     my @clauses;
431     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
432         my $type = ref( $obj ) || $obj;
433         my $clause = "ACL.ObjectType = '$type'";
434
435         if ( ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id ) {
436             $clause .= " AND ACL.ObjectId = ". $obj->id;
437         }
438
439         push @clauses, "($clause)";
440     }
441     if ( @clauses ) {
442         $query .= " AND (". join( ' OR ', @clauses ) .")";
443     }
444
445     $self->_Handle->ApplyLimits( \$query, 1 );
446     my ($hit, $obj, $id) = $self->_Handle->FetchResult( $query );
447     return (0) unless $hit;
448
449     $obj .= "-$id" if $id;
450     return (1, $obj);
451 }
452
453 sub _HasRoleRight
454 {
455     my $self = shift;
456     my %args = (
457         Right        => undef,
458         EquivObjects => [],
459         @_
460     );
461     my $right = $args{'Right'};
462
463     my $query =
464       "SELECT ACL.id " .
465       "FROM ACL, Groups, Principals, CachedGroupMembers WHERE " .
466
467       # Only find superuser or rights with the name $right
468       "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
469
470       # Never find disabled things
471       . "AND Principals.Disabled = 0 "
472       . "AND CachedGroupMembers.Disabled = 0 "
473
474       # We always grant rights to Groups
475       . "AND Principals.id = Groups.id "
476       . "AND Principals.PrincipalType = 'Group' "
477
478       # See if the principal is a member of the group recursively or _is the rightholder_
479       # never find recursively disabled group members
480       # also, check to see if the right is being granted _directly_ to this principal,
481       #  as is the case when we want to look up group rights
482       . "AND Principals.id = CachedGroupMembers.GroupId "
483       . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
484       . "AND ACL.PrincipalType = Groups.Type ";
485
486     my (@object_clauses);
487     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
488         my $type = ref($obj)? ref($obj): $obj;
489         my $id;
490         $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
491
492         my $object_clause = "ACL.ObjectType = '$type'";
493         $object_clause   .= " AND ACL.ObjectId = $id" if $id;
494         push @object_clauses, "($object_clause)";
495     }
496     # find ACLs that are related to our objects only
497     $query .= " AND (". join( ' OR ', @object_clauses ) .")";
498
499     # because of mysql bug in versions up to 5.0.45 we do one query per object
500     # each query should be faster on any DB as it uses indexes more effective
501     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
502         my $type = ref($obj)? ref($obj): $obj;
503         my $id;
504         $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
505
506         my $tmp = $query;
507         $tmp .= " AND Groups.Domain = '$type-Role'";
508         # XXX: Groups.Instance is VARCHAR in DB, we should quote value
509         # if we want mysql 4.0 use indexes here. we MUST convert that
510         # field to integer and drop this quotes.
511         $tmp .= " AND Groups.Instance = '$id'" if $id;
512
513         $self->_Handle->ApplyLimits( \$tmp, 1 );
514         my ($hit) = $self->_Handle->FetchResult( $tmp );
515         return (1) if $hit;
516     }
517
518     return 0;
519 }
520
521 # }}}
522
523 # }}}
524
525 # {{{ ACL caching
526
527
528 # {{{ InvalidateACLCache
529
530 =head2 InvalidateACLCache
531
532 Cleans out and reinitializes the user rights cache
533
534 =cut
535
536 sub InvalidateACLCache {
537     $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
538     $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60);
539
540 }
541
542 # }}}
543
544 # }}}
545
546
547 # {{{ _GetPrincipalTypeForACL
548
549 =head2 _GetPrincipalTypeForACL
550
551 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type, 
552 return that. if it has no type, return group.
553
554 =cut
555
556 sub _GetPrincipalTypeForACL {
557     my $self = shift;
558     my $type;    
559     if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
560         $type = $self->Object->Type;
561     }
562     else {
563         $type = $self->PrincipalType;
564     }
565
566     return($type);
567 }
568
569 # }}}
570
571 # {{{ _ReferenceId
572
573 =head2 _ReferenceId
574
575 Returns a list uniquely representing an object or normal scalar.
576
577 For scalars, its string value is returned; for objects that has an
578 id() method, its class name and Id are returned as a string separated by a "-".
579
580 =cut
581
582 sub _ReferenceId {
583     my $scalar = shift;
584
585     # just return the value for non-objects
586     return $scalar unless UNIVERSAL::can($scalar, 'id');
587
588     return ref($scalar) unless $scalar->id;
589
590     # an object -- return the class and id
591     return(ref($scalar)."-". $scalar->id);
592 }
593
594 # }}}
595
596 1;