e0646a71ff256369ee5deae689f26cda31fca89b
[freeside.git] / rt / lib / RT / Principal_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4
5 # This software is Copyright (c) 1996-2009 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/licenses/old-licenses/gpl-2.0.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
51 package RT::Principal;
52
53 use strict;
54 use warnings;
55
56 use Cache::Simple::TimedExpiry;
57
58
59 use RT;
60 use RT::Group;
61 use RT::User;
62
63 # Set up the ACL cache on startup
64 our $_ACL_CACHE;
65 InvalidateACLCache();
66
67 # {{{ IsGroup
68
69 =head2 IsGroup
70
71 Returns true if this principal is a group. 
72 Returns undef, otherwise
73
74 =cut
75
76 sub IsGroup {
77     my $self = shift;
78     if ( defined $self->PrincipalType && 
79             $self->PrincipalType eq 'Group' ) {
80         return 1;
81     }
82     return undef;
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 = (
156         Right => undef,
157         Object => undef,
158         @_
159     );
160
161     #ACL check handled in ACE.pm
162     my $ace = RT::ACE->new( $self->CurrentUser );
163
164     my $type = $self->_GetPrincipalTypeForACL();
165
166     # If it's a user, we really want to grant the right to their 
167     # user equivalence group
168     return $ace->Create(
169         RightName     => $args{'Right'},
170         Object        => $args{'Object'},
171         PrincipalType => $type,
172         PrincipalId   => $self->Id,
173     );
174 }
175 # }}}
176
177 # {{{ RevokeRight
178
179 =head2 RevokeRight { Right => "RightName", Object => "object" }
180
181 Delete a right that a user has 
182
183
184    Returns a tuple of (STATUS, MESSAGE);  If the call succeeded, STATUS is true. Otherwise it's 
185       false.
186
187
188 =cut
189
190 sub RevokeRight {
191
192     my $self = shift;
193     my %args = (
194         Right  => undef,
195         Object => undef,
196         @_
197     );
198
199     #if we haven't specified any sort of right, we're talking about a global right
200     if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
201         $args{'Object'} = $RT::System;
202     }
203     #ACL check handled in ACE.pm
204     my $type = $self->_GetPrincipalTypeForACL();
205
206     my $ace = RT::ACE->new( $self->CurrentUser );
207     my ($status, $msg) = $ace->LoadByValues(
208         RightName     => $args{'Right'},
209         Object        => $args{'Object'},
210         PrincipalType => $type,
211         PrincipalId   => $self->Id
212     );
213     return ($status, $msg) unless $status;
214     return $ace->Delete;
215 }
216
217 # }}}
218
219 # {{{ sub _CleanupInvalidDelegations
220
221 =head2 sub _CleanupInvalidDelegations { InsideTransaction => undef }
222
223 Revokes all ACE entries delegated by this principal which are
224 inconsistent with this principal's current delegation rights.  Does
225 not perform permission checks, but takes no action and returns success
226 if this principal still retains DelegateRights.  Should only ever be
227 called from inside the RT library.
228
229 If this principal is a group, recursively calls this method on each
230 cached user member of itself.
231
232 If called from inside a transaction, specify a true value for the
233 InsideTransaction parameter.
234
235 Returns a true value if the deletion succeeded; returns a false value
236 and logs an internal error if the deletion fails (should not happen).
237
238 =cut
239
240 # This is currently just a stub for the methods of the same name in
241 # RT::User and RT::Group.
242
243 sub _CleanupInvalidDelegations {
244     my $self = shift;
245     unless ( $self->Id ) {
246         $RT::Logger->warning("Principal not loaded.");
247         return (undef);
248     }
249     return ($self->Object->_CleanupInvalidDelegations(@_));
250 }
251
252 # }}}
253
254 # {{{ sub HasRight
255
256 =head2 sub HasRight (Right => 'right' Object => undef)
257
258
259 Checks to see whether this principal has the right "Right" for the Object
260 specified. If the Object parameter is omitted, checks to see whether the 
261 user has the right globally.
262
263 This still hard codes to check to see if a user has queue-level rights
264 if we ask about a specific ticket.
265
266
267 This takes the params:
268
269     Right => name of a right
270
271     And either:
272
273     Object => an RT style object (->id will get its id)
274
275
276 Returns 1 if a matching ACE was found.
277
278 Returns undef if no ACE was found.
279
280 =cut
281
282 sub HasRight {
283
284     my $self = shift;
285     my %args = (
286         Right        => undef,
287         Object       => undef,
288         EquivObjects => undef,
289         @_,
290     );
291
292     unless ( $args{'Right'} ) {
293         $RT::Logger->crit("HasRight called without a right");
294         return (undef);
295     }
296
297     my $canonic_name = RT::ACE->CanonicalizeRightName( $args{'Right'} );
298     unless ( $canonic_name ) {
299         $RT::Logger->error("Invalid right. Couldn't canonicalize right '$args{'Right'}'");
300         return undef;
301     }
302     $args{'Right'} = $canonic_name;
303
304     $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
305         if $args{'EquivObjects'};
306
307     if ( $self->Disabled ) {
308         $RT::Logger->debug( "Disabled User #"
309               . $self->id
310               . " failed access check for "
311               . $args{'Right'} );
312         return (undef);
313     }
314
315     if (   defined( $args{'Object'} )
316         && UNIVERSAL::can( $args{'Object'}, 'id' )
317         && $args{'Object'}->id ) {
318
319         push @{ $args{'EquivObjects'} }, $args{'Object'};
320     }
321     else {
322         $RT::Logger->crit("HasRight called with no valid object");
323         return (undef);
324     }
325
326
327     unshift @{ $args{'EquivObjects'} }, $args{'Object'}->ACLEquivalenceObjects;
328
329     unshift @{ $args{'EquivObjects'} }, $RT::System
330         unless $self->can('_IsOverrideGlobalACL')
331                && $self->_IsOverrideGlobalACL( $args{'Object'} );
332
333     # {{{ If we've cached a win or loss for this lookup say so
334
335     # Construct a hashkeys to cache decisions:
336     # 1) full_hashkey - key for any result and for full combination of uid, right and objects
337     # 2) short_hashkey - one key for each object to store positive results only, it applies
338     # only to direct group rights and partly to role rights
339     my $self_id = $self->id;
340     my $full_hashkey = join ";:;", $self_id, $args{'Right'};
341     foreach ( @{ $args{'EquivObjects'} } ) {
342         my $ref_id = _ReferenceId($_);
343         $full_hashkey .= ";:;$ref_id";
344
345         my $short_hashkey = join ";:;", $self_id, $args{'Right'}, $ref_id;
346         my $cached_answer = $_ACL_CACHE->fetch($short_hashkey);
347         return $cached_answer > 0 if defined $cached_answer;
348     }
349
350     {
351         my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
352         return $cached_answer > 0 if defined $cached_answer;
353     }
354
355
356     my ($hitcount, $via_obj) = $self->_HasRight( %args );
357
358     $_ACL_CACHE->set( $full_hashkey => $hitcount? 1: -1 );
359     $_ACL_CACHE->set( "$self_id;:;$args{'Right'};:;$via_obj" => 1 )
360         if $via_obj && $hitcount;
361
362     return ($hitcount);
363 }
364
365 =head2 _HasRight
366
367 Low level HasRight implementation, use HasRight method instead.
368
369 =cut
370
371 sub _HasRight
372 {
373     my $self = shift;
374     {
375         my ($hit, @other) = $self->_HasGroupRight( @_ );
376         return ($hit, @other) if $hit;
377     }
378     {
379         my ($hit, @other) = $self->_HasRoleRight( @_ );
380         return ($hit, @other) if $hit;
381     }
382     return (0);
383 }
384
385 # this method handles role rights partly in situations
386 # where user plays role X on an object and as well the right is
387 # assigned to this role X of the object, for example right CommentOnTicket
388 # is granted to Cc role of a queue and user is in cc list of the queue
389 sub _HasGroupRight
390 {
391     my $self = shift;
392     my %args = (
393         Right        => undef,
394         EquivObjects => [],
395         @_
396     );
397     my $right = $args{'Right'};
398
399     my $query =
400       "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId " .
401       "FROM ACL, Principals, CachedGroupMembers WHERE " .
402
403       # Only find superuser or rights with the name $right
404       "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
405
406       # Never find disabled groups.
407       . "AND Principals.id = ACL.PrincipalId "
408       . "AND Principals.PrincipalType = 'Group' "
409       . "AND Principals.Disabled = 0 "
410
411       # See if the principal is a member of the group recursively or _is the rightholder_
412       # never find recursively disabled group members
413       # also, check to see if the right is being granted _directly_ to this principal,
414       #  as is the case when we want to look up group rights
415       . "AND CachedGroupMembers.GroupId  = ACL.PrincipalId "
416       . "AND CachedGroupMembers.GroupId  = Principals.id "
417       . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
418       . "AND CachedGroupMembers.Disabled = 0 ";
419
420     my @clauses;
421     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
422         my $type = ref( $obj ) || $obj;
423         my $clause = "ACL.ObjectType = '$type'";
424
425         if ( ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id ) {
426             $clause .= " AND ACL.ObjectId = ". $obj->id;
427         }
428
429         push @clauses, "($clause)";
430     }
431     if ( @clauses ) {
432         $query .= " AND (". join( ' OR ', @clauses ) .")";
433     }
434
435     $self->_Handle->ApplyLimits( \$query, 1 );
436     my ($hit, $obj, $id) = $self->_Handle->FetchResult( $query );
437     return (0) unless $hit;
438
439     $obj .= "-$id" if $id;
440     return (1, $obj);
441 }
442
443 sub _HasRoleRight
444 {
445     my $self = shift;
446     my %args = (
447         Right        => undef,
448         EquivObjects => [],
449         @_
450     );
451     my $right = $args{'Right'};
452
453     my $query =
454       "SELECT ACL.id " .
455       "FROM ACL, Groups, Principals, CachedGroupMembers WHERE " .
456
457       # Only find superuser or rights with the name $right
458       "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
459
460       # Never find disabled things
461       . "AND Principals.Disabled = 0 "
462       . "AND CachedGroupMembers.Disabled = 0 "
463
464       # We always grant rights to Groups
465       . "AND Principals.id = Groups.id "
466       . "AND Principals.PrincipalType = 'Group' "
467
468       # See if the principal is a member of the group recursively or _is the rightholder_
469       # never find recursively disabled group members
470       # also, check to see if the right is being granted _directly_ to this principal,
471       #  as is the case when we want to look up group rights
472       . "AND Principals.id = CachedGroupMembers.GroupId "
473       . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
474       . "AND ACL.PrincipalType = Groups.Type ";
475
476     my (@object_clauses);
477     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
478         my $type = ref($obj)? ref($obj): $obj;
479         my $id;
480         $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
481
482         my $object_clause = "ACL.ObjectType = '$type'";
483         $object_clause   .= " AND ACL.ObjectId = $id" if $id;
484         push @object_clauses, "($object_clause)";
485     }
486     # find ACLs that are related to our objects only
487     $query .= " AND (". join( ' OR ', @object_clauses ) .")";
488
489     # because of mysql bug in versions up to 5.0.45 we do one query per object
490     # each query should be faster on any DB as it uses indexes more effective
491     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
492         my $type = ref($obj)? ref($obj): $obj;
493         my $id;
494         $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
495
496         my $tmp = $query;
497         $tmp .= " AND Groups.Domain = '$type-Role'";
498         # XXX: Groups.Instance is VARCHAR in DB, we should quote value
499         # if we want mysql 4.0 use indexes here. we MUST convert that
500         # field to integer and drop this quotes.
501         $tmp .= " AND Groups.Instance = '$id'" if $id;
502
503         $self->_Handle->ApplyLimits( \$tmp, 1 );
504         my ($hit) = $self->_Handle->FetchResult( $tmp );
505         return (1) if $hit;
506     }
507
508     return 0;
509 }
510
511 # }}}
512
513 # }}}
514
515 # {{{ ACL caching
516
517
518 # {{{ InvalidateACLCache
519
520 =head2 InvalidateACLCache
521
522 Cleans out and reinitializes the user rights cache
523
524 =cut
525
526 sub InvalidateACLCache {
527     $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
528     my $lifetime;
529     $lifetime = $RT::Config->Get('ACLCacheLifetime') if $RT::Config;
530     $_ACL_CACHE->expire_after( $lifetime || 60 );
531 }
532
533 # }}}
534
535 # }}}
536
537
538 # {{{ _GetPrincipalTypeForACL
539
540 =head2 _GetPrincipalTypeForACL
541
542 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type, 
543 return that. if it has no type, return group.
544
545 =cut
546
547 sub _GetPrincipalTypeForACL {
548     my $self = shift;
549     my $type;    
550     if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
551         $type = $self->Object->Type;
552     }
553     else {
554         $type = $self->PrincipalType;
555     }
556
557     return($type);
558 }
559
560 # }}}
561
562 # {{{ _ReferenceId
563
564 =head2 _ReferenceId
565
566 Returns a list uniquely representing an object or normal scalar.
567
568 For scalars, its string value is returned; for objects that has an
569 id() method, its class name and Id are returned as a string separated by a "-".
570
571 =cut
572
573 sub _ReferenceId {
574     my $scalar = shift;
575
576     # just return the value for non-objects
577     return $scalar unless UNIVERSAL::can($scalar, 'id');
578
579     return ref($scalar) unless $scalar->id;
580
581     # an object -- return the class and id
582     return(ref($scalar)."-". $scalar->id);
583 }
584
585 # }}}
586
587 1;