88e721ee3d934651347a1b8c017e8c55d52a2ddc
[freeside.git] / rt / lib / RT / Principal_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 #                                          <sales@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 # backcompat for 3.8.8 and before
244 *_CleanupInvalidDelegations = \&CleanupInvalidDelegations;
245
246 sub CleanupInvalidDelegations {
247     my $self = shift;
248     unless ( $self->Id ) {
249         $RT::Logger->warning("Principal not loaded.");
250         return (undef);
251     }
252     return ($self->Object->CleanupInvalidDelegations(@_));
253 }
254
255
256 # }}}
257
258 # {{{ sub HasRight
259
260 =head2 sub HasRight (Right => 'right' Object => undef)
261
262
263 Checks to see whether this principal has the right "Right" for the Object
264 specified. If the Object parameter is omitted, checks to see whether the 
265 user has the right globally.
266
267 This still hard codes to check to see if a user has queue-level rights
268 if we ask about a specific ticket.
269
270
271 This takes the params:
272
273     Right => name of a right
274
275     And either:
276
277     Object => an RT style object (->id will get its id)
278
279
280 Returns 1 if a matching ACE was found.
281
282 Returns undef if no ACE was found.
283
284 =cut
285
286 sub HasRight {
287
288     my $self = shift;
289     my %args = (
290         Right        => undef,
291         Object       => undef,
292         EquivObjects => undef,
293         @_,
294     );
295
296     unless ( $args{'Right'} ) {
297         $RT::Logger->crit("HasRight called without a right");
298         return (undef);
299     }
300
301     my $canonic_name = RT::ACE->CanonicalizeRightName( $args{'Right'} );
302     unless ( $canonic_name ) {
303         $RT::Logger->error("Invalid right. Couldn't canonicalize right '$args{'Right'}'");
304         return undef;
305     }
306     $args{'Right'} = $canonic_name;
307
308     $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
309         if $args{'EquivObjects'};
310
311     if ( $self->Disabled ) {
312         $RT::Logger->debug( "Disabled User #"
313               . $self->id
314               . " failed access check for "
315               . $args{'Right'} );
316         return (undef);
317     }
318
319     if (   defined( $args{'Object'} )
320         && UNIVERSAL::can( $args{'Object'}, 'id' )
321         && $args{'Object'}->id ) {
322
323         push @{ $args{'EquivObjects'} }, $args{'Object'};
324     }
325     else {
326         $RT::Logger->crit("HasRight called with no valid object");
327         return (undef);
328     }
329
330
331     unshift @{ $args{'EquivObjects'} }, $args{'Object'}->ACLEquivalenceObjects;
332
333     unshift @{ $args{'EquivObjects'} }, $RT::System
334         unless $self->can('_IsOverrideGlobalACL')
335                && $self->_IsOverrideGlobalACL( $args{'Object'} );
336
337     # {{{ If we've cached a win or loss for this lookup say so
338
339     # Construct a hashkeys to cache decisions:
340     # 1) full_hashkey - key for any result and for full combination of uid, right and objects
341     # 2) short_hashkey - one key for each object to store positive results only, it applies
342     # only to direct group rights and partly to role rights
343     my $self_id = $self->id;
344     my $full_hashkey = join ";:;", $self_id, $args{'Right'};
345     foreach ( @{ $args{'EquivObjects'} } ) {
346         my $ref_id = _ReferenceId($_);
347         $full_hashkey .= ";:;$ref_id";
348
349         my $short_hashkey = join ";:;", $self_id, $args{'Right'}, $ref_id;
350         my $cached_answer = $_ACL_CACHE->fetch($short_hashkey);
351         return $cached_answer > 0 if defined $cached_answer;
352     }
353
354     {
355         my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
356         return $cached_answer > 0 if defined $cached_answer;
357     }
358
359
360     my ($hitcount, $via_obj) = $self->_HasRight( %args );
361
362     $_ACL_CACHE->set( $full_hashkey => $hitcount? 1: -1 );
363     $_ACL_CACHE->set( "$self_id;:;$args{'Right'};:;$via_obj" => 1 )
364         if $via_obj && $hitcount;
365
366     return ($hitcount);
367 }
368
369 =head2 _HasRight
370
371 Low level HasRight implementation, use HasRight method instead.
372
373 =cut
374
375 sub _HasRight
376 {
377     my $self = shift;
378     {
379         my ($hit, @other) = $self->_HasGroupRight( @_ );
380         return ($hit, @other) if $hit;
381     }
382     {
383         my ($hit, @other) = $self->_HasRoleRight( @_ );
384         return ($hit, @other) if $hit;
385     }
386     return (0);
387 }
388
389 # this method handles role rights partly in situations
390 # where user plays role X on an object and as well the right is
391 # assigned to this role X of the object, for example right CommentOnTicket
392 # is granted to Cc role of a queue and user is in cc list of the queue
393 sub _HasGroupRight
394 {
395     my $self = shift;
396     my %args = (
397         Right        => undef,
398         EquivObjects => [],
399         @_
400     );
401     my $right = $args{'Right'};
402
403     my $query =
404       "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId " .
405       "FROM ACL, Principals, CachedGroupMembers WHERE " .
406
407       # Only find superuser or rights with the name $right
408       "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
409
410       # Never find disabled groups.
411       . "AND Principals.id = ACL.PrincipalId "
412       . "AND Principals.PrincipalType = 'Group' "
413       . "AND Principals.Disabled = 0 "
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 CachedGroupMembers.GroupId  = ACL.PrincipalId "
420       . "AND CachedGroupMembers.GroupId  = Principals.id "
421       . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
422       . "AND CachedGroupMembers.Disabled = 0 ";
423
424     my @clauses;
425     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
426         my $type = ref( $obj ) || $obj;
427         my $clause = "ACL.ObjectType = '$type'";
428
429         if ( ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id ) {
430             $clause .= " AND ACL.ObjectId = ". $obj->id;
431         }
432
433         push @clauses, "($clause)";
434     }
435     if ( @clauses ) {
436         $query .= " AND (". join( ' OR ', @clauses ) .")";
437     }
438
439     $self->_Handle->ApplyLimits( \$query, 1 );
440     my ($hit, $obj, $id) = $self->_Handle->FetchResult( $query );
441     return (0) unless $hit;
442
443     $obj .= "-$id" if $id;
444     return (1, $obj);
445 }
446
447 sub _HasRoleRight
448 {
449     my $self = shift;
450     my %args = (
451         Right        => undef,
452         EquivObjects => [],
453         @_
454     );
455
456     my @roles = $self->RolesWithRight( %args );
457     return 0 unless @roles;
458
459     my $right = $args{'Right'};
460
461     my $query =
462       "SELECT Groups.id "
463       . "FROM Groups, Principals, CachedGroupMembers WHERE "
464
465       # Never find disabled things
466       . "Principals.Disabled = 0 "
467       . "AND CachedGroupMembers.Disabled = 0 "
468
469       # We always grant rights to Groups
470       . "AND Principals.id = Groups.id "
471       . "AND Principals.PrincipalType = 'Group' "
472
473       # See if the principal is a member of the group recursively or _is the rightholder_
474       # never find recursively disabled group members
475       # also, check to see if the right is being granted _directly_ to this principal,
476       #  as is the case when we want to look up group rights
477       . "AND Principals.id = CachedGroupMembers.GroupId "
478       . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
479
480       . "AND (". join(' OR ', map "Groups.Type = '$_'", @roles ) .")"
481     ;
482
483     my (@object_clauses);
484     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
485         my $type = ref($obj)? ref($obj): $obj;
486         my $id;
487         $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
488
489         my $clause = "Groups.Domain = '$type-Role'";
490         # XXX: Groups.Instance is VARCHAR in DB, we should quote value
491         # if we want mysql 4.0 use indexes here. we MUST convert that
492         # field to integer and drop this quotes.
493         $clause .= " AND Groups.Instance = '$id'" if $id;
494         push @object_clauses, "($clause)";
495     }
496     $query .= " AND (". join( ' OR ', @object_clauses ) .")";
497
498     $self->_Handle->ApplyLimits( \$query, 1 );
499     my ($hit) = $self->_Handle->FetchResult( $query );
500     return (1) if $hit;
501
502     return 0;
503 }
504
505 =head2 RolesWithRight
506
507 Returns list with names of roles that have right on
508 set of objects. Takes Right, EquiveObjects,
509 IncludeSystemRights and IncludeSuperusers arguments.
510
511 IncludeSystemRights is true by default, rights
512 granted on system level are not accouned when option
513 is set to false value.
514
515 IncludeSuperusers is true by default, SuperUser right
516 is not checked if it's set to false value.
517
518 =cut
519
520 sub RolesWithRight {
521     my $self = shift;
522     my %args = (
523         Right               => undef,
524         IncludeSystemRights => 1,
525         IncludeSuperusers   => 1,
526         EquivObjects        => [],
527         @_
528     );
529     my $right = $args{'Right'};
530
531     my $query =
532         "SELECT DISTINCT PrincipalType FROM ACL"
533         # Only find superuser or rights with the name $right
534         ." WHERE ( RightName = '$right' "
535         # Check SuperUser if we were asked to
536         . ($args{'IncludeSuperusers'}? "OR RightName = 'SuperUser' " : '' )
537         .")"
538         # we need only roles
539         ." AND PrincipalType != 'Group'"
540     ;
541
542     # skip rights granted on system level if we were asked to
543     unless ( $args{'IncludeSystemRights'} ) {
544         $query .= " AND ObjectType != 'RT::System'";
545     }
546
547     my (@object_clauses);
548     foreach my $obj ( @{ $args{'EquivObjects'} } ) {
549         my $type = ref($obj)? ref($obj): $obj;
550         my $id;
551         $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
552
553         my $object_clause = "ObjectType = '$type'";
554         $object_clause   .= " AND ObjectId = $id" if $id;
555         push @object_clauses, "($object_clause)";
556     }
557     # find ACLs that are related to our objects only
558     $query .= " AND (". join( ' OR ', @object_clauses ) .")"
559         if @object_clauses;
560
561     my $dbh = $RT::Handle->dbh;
562     my $roles = $dbh->selectcol_arrayref($query);
563     unless ( $roles ) {
564         $RT::Logger->warning( $dbh->errstr );
565         return ();
566     }
567     return @$roles;
568 }
569
570 # }}}
571
572 # }}}
573
574 # {{{ ACL caching
575
576
577 # {{{ InvalidateACLCache
578
579 =head2 InvalidateACLCache
580
581 Cleans out and reinitializes the user rights cache
582
583 =cut
584
585 sub InvalidateACLCache {
586     $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
587     my $lifetime;
588     $lifetime = $RT::Config->Get('ACLCacheLifetime') if $RT::Config;
589     $_ACL_CACHE->expire_after( $lifetime || 60 );
590 }
591
592 # }}}
593
594 # }}}
595
596
597 # {{{ _GetPrincipalTypeForACL
598
599 =head2 _GetPrincipalTypeForACL
600
601 Gets the principal type. if it's a user, it's a user. if it's a role group and it has a Type, 
602 return that. if it has no type, return group.
603
604 =cut
605
606 sub _GetPrincipalTypeForACL {
607     my $self = shift;
608     my $type;    
609     if ($self->PrincipalType eq 'Group' && $self->Object->Domain =~ /Role$/) {
610         $type = $self->Object->Type;
611     }
612     else {
613         $type = $self->PrincipalType;
614     }
615
616     return($type);
617 }
618
619 # }}}
620
621 # {{{ _ReferenceId
622
623 =head2 _ReferenceId
624
625 Returns a list uniquely representing an object or normal scalar.
626
627 For scalars, its string value is returned; for objects that has an
628 id() method, its class name and Id are returned as a string separated by a "-".
629
630 =cut
631
632 sub _ReferenceId {
633     my $scalar = shift;
634
635     # just return the value for non-objects
636     return $scalar unless UNIVERSAL::can($scalar, 'id');
637
638     return ref($scalar) unless $scalar->id;
639
640     # an object -- return the class and id
641     return(ref($scalar)."-". $scalar->id);
642 }
643
644 # }}}
645
646 1;