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