import rt 3.8.10
[freeside.git] / rt / lib / RT / ACE_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 =head1 SYNOPSIS
50
51   use RT::ACE;
52   my $ace = new RT::ACE($CurrentUser);
53
54
55 =head1 DESCRIPTION
56
57
58
59 =head1 METHODS
60
61
62 =cut
63
64
65 package RT::ACE;
66
67 use strict;
68 no warnings qw(redefine);
69 use RT::Principals;
70 use RT::Queues;
71 use RT::Groups;
72
73 use vars qw (
74   %LOWERCASERIGHTNAMES
75   %OBJECT_TYPES
76   %TICKET_METAPRINCIPALS
77 );
78
79
80 # {{{ Descriptions of rights
81
82 =head1 Rights
83
84 # Queue rights are the sort of queue rights that can only be granted
85 # to real people or groups
86
87
88
89 =cut
90
91
92
93
94 # }}}
95
96 # {{{ Descriptions of principals
97
98 %TICKET_METAPRINCIPALS = (
99     Owner     => 'The owner of a ticket',                             # loc_pair
100     Requestor => 'The requestor of a ticket',                         # loc_pair
101     Cc        => 'The CC of a ticket',                                # loc_pair
102     AdminCc   => 'The administrative CC of a ticket',                 # loc_pair
103 );
104
105 # }}}
106
107
108 # {{{ sub LoadByValues
109
110 =head2 LoadByValues PARAMHASH
111
112 Load an ACE by specifying a paramhash with the following fields:
113
114               PrincipalId => undef,
115               PrincipalType => undef,
116               RightName => undef,
117
118         And either:
119
120               Object => undef,
121
122             OR
123
124               ObjectType => undef,
125               ObjectId => undef
126
127 =cut
128
129 sub LoadByValues {
130     my $self = shift;
131     my %args = ( PrincipalId   => undef,
132                  PrincipalType => undef,
133                  RightName     => undef,
134                  Object    => undef,
135                  ObjectId    => undef,
136                  ObjectType    => undef,
137                  @_ );
138
139     if ( $args{'RightName'} ) {
140         my $canonic_name = $self->CanonicalizeRightName( $args{'RightName'} );
141         unless ( $canonic_name ) {
142             return ( 0, $self->loc("Invalid right. Couldn't canonicalize right '[_1]'", $args{'RightName'}) );
143         }
144         $args{'RightName'} = $canonic_name;
145     }
146
147     my $princ_obj;
148     ( $princ_obj, $args{'PrincipalType'} ) =
149       $self->_CanonicalizePrincipal( $args{'PrincipalId'},
150                                      $args{'PrincipalType'} );
151
152     unless ( $princ_obj->id ) {
153         return ( 0,
154                  $self->loc( 'Principal [_1] not found.', $args{'PrincipalId'} )
155         );
156     }
157
158     my ($object, $object_type, $object_id) = $self->_ParseObjectArg( %args );
159     unless( $object ) {
160         return ( 0, $self->loc("System error. Right not granted.") );
161     }
162
163     $self->LoadByCols( PrincipalId   => $princ_obj->Id,
164                        PrincipalType => $args{'PrincipalType'},
165                        RightName     => $args{'RightName'},
166                        ObjectType    => $object_type,
167                        ObjectId      => $object_id);
168
169     #If we couldn't load it.
170     unless ( $self->Id ) {
171         return ( 0, $self->loc("ACE not found") );
172     }
173
174     # if we could
175     return ( $self->Id, $self->loc("Right Loaded") );
176
177 }
178
179 # }}}
180
181 # {{{ sub Create
182
183 =head2 Create <PARAMS>
184
185 PARAMS is a parameter hash with the following elements:
186
187    PrincipalId => The id of an RT::Principal object
188    PrincipalType => "User" "Group" or any Role type
189    RightName => the name of a right. in any case
190    DelegatedBy => The Principal->Id of the user delegating the right
191    DelegatedFrom => The id of the ACE which this new ACE is delegated from
192
193
194     Either:
195
196    Object => An object to create rights for. ususally, an RT::Queue or RT::Group
197              This should always be a DBIx::SearchBuilder::Record subclass
198
199         OR
200
201    ObjectType => the type of the object in question (ref ($object))
202    ObjectId => the id of the object in question $object->Id
203
204
205
206    Returns a tuple of (STATUS, MESSAGE);  If the call succeeded, STATUS is true. Otherwise it's false.
207
208
209
210 =cut
211
212 sub Create {
213     my $self = shift;
214     my %args = (
215         PrincipalId   => undef,
216         PrincipalType => undef,
217         RightName     => undef,
218         Object        => undef,
219         @_
220     );
221
222     unless ( $args{'RightName'} ) {
223         return ( 0, $self->loc('No right specified') );
224     }
225
226     #if we haven't specified any sort of right, we're talking about a global right
227     if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
228         $args{'Object'} = $RT::System;
229     }
230     ($args{'Object'}, $args{'ObjectType'}, $args{'ObjectId'}) = $self->_ParseObjectArg( %args );
231     unless( $args{'Object'} ) {
232         return ( 0, $self->loc("System error. Right not granted.") );
233     }
234
235     # {{{ Validate the principal
236     my $princ_obj;
237     ( $princ_obj, $args{'PrincipalType'} ) =
238       $self->_CanonicalizePrincipal( $args{'PrincipalId'},
239                                      $args{'PrincipalType'} );
240
241     unless ( $princ_obj->id ) {
242         return ( 0,
243                  $self->loc( 'Principal [_1] not found.', $args{'PrincipalId'} )
244         );
245     }
246
247     # }}}
248
249     # {{{ Check the ACL
250
251     if (ref( $args{'Object'}) eq 'RT::Group' ) {
252         unless ( $self->CurrentUser->HasRight( Object => $args{'Object'},
253                                                   Right => 'AdminGroup' )
254           ) {
255             return ( 0, $self->loc('Permission Denied') );
256         }
257     }
258
259     else {
260         unless ( $self->CurrentUser->HasRight( Object => $args{'Object'}, Right => 'ModifyACL' )) {
261             return ( 0, $self->loc('Permission Denied') );
262         }
263     }
264     # }}}
265
266     # {{{ Canonicalize and check the right name
267     my $canonic_name = $self->CanonicalizeRightName( $args{'RightName'} );
268     unless ( $canonic_name ) {
269         return ( 0, $self->loc("Invalid right. Couldn't canonicalize right '[_1]'", $args{'RightName'}) );
270     }
271     $args{'RightName'} = $canonic_name;
272
273     #check if it's a valid RightName
274     if ( $args{'Object'}->can('AvailableRights') ) {
275         my $available = $args{'Object'}->AvailableRights;
276         unless ( grep $_ eq $args{'RightName'}, map $self->CanonicalizeRightName( $_ ), keys %$available ) {
277             $RT::Logger->warning(
278                 "Couldn't validate right name '$args{'RightName'}'"
279                 ." for object of ". ref( $args{'Object'} ) ." class"
280             );
281             return ( 0, $self->loc('Invalid right') );
282         }
283     }
284     # }}}
285
286     # Make sure the right doesn't already exist.
287     $self->LoadByCols( PrincipalId   => $princ_obj->id,
288                        PrincipalType => $args{'PrincipalType'},
289                        RightName     => $args{'RightName'},
290                        ObjectType    => $args{'ObjectType'},
291                        ObjectId      => $args{'ObjectId'},
292                        DelegatedBy   => 0,
293                        DelegatedFrom => 0 );
294     if ( $self->Id ) {
295         return ( 0, $self->loc('That principal already has that right') );
296     }
297
298     my $id = $self->SUPER::Create( PrincipalId   => $princ_obj->id,
299                                    PrincipalType => $args{'PrincipalType'},
300                                    RightName     => $args{'RightName'},
301                                    ObjectType    => ref( $args{'Object'} ),
302                                    ObjectId      => $args{'Object'}->id,
303                                    DelegatedBy   => 0,
304                                    DelegatedFrom => 0 );
305
306     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
307     RT::Principal->InvalidateACLCache();
308
309     if ( $id ) {
310         return ( $id, $self->loc('Right Granted') );
311     }
312     else {
313         return ( 0, $self->loc('System error. Right not granted.') );
314     }
315 }
316
317 # }}}
318
319 # {{{ sub Delegate
320
321 =head2 Delegate <PARAMS>
322
323 This routine delegates the current ACE to a principal specified by the
324 B<PrincipalId>  parameter.
325
326 Returns an error if the current user doesn't have the right to be delegated
327 or doesn't have the right to delegate rights.
328
329 Always returns a tuple of (ReturnValue, Message)
330
331
332 =cut
333
334 sub Delegate {
335     my $self = shift;
336     my %args = ( PrincipalId => undef,
337                  @_ );
338
339     unless ( $self->Id ) {
340         return ( 0, $self->loc("Right not loaded.") );
341     }
342     my $princ_obj;
343     ( $princ_obj, $args{'PrincipalType'} ) =
344       $self->_CanonicalizePrincipal( $args{'PrincipalId'},
345                                      $args{'PrincipalType'} );
346
347     unless ( $princ_obj->id ) {
348         return ( 0,
349                  $self->loc( 'Principal [_1] not found.', $args{'PrincipalId'} )
350         );
351     }
352
353     # }}}
354
355     # {{{ Check the ACL
356
357     # First, we check to se if the user is delegating rights and
358     # they have the permission to
359     unless ( $self->CurrentUser->HasRight(Right => 'DelegateRights', Object => $self->Object) ) {
360         return ( 0, $self->loc("Permission Denied") );
361     }
362
363     unless ( $self->PrincipalObj->IsGroup ) {
364         return ( 0, $self->loc("System Error") );
365     }
366     unless ( $self->PrincipalObj->Object->HasMemberRecursively(
367                                                 $self->CurrentUser->PrincipalObj
368              )
369       ) {
370         return ( 0, $self->loc("Permission Denied") );
371     }
372
373     # }}}
374
375     my $concurrency_check = RT::ACE->new($RT::SystemUser);
376     $concurrency_check->Load( $self->Id );
377     unless ( $concurrency_check->Id ) {
378         $RT::Logger->crit(
379                    "Trying to delegate a right which had already been deleted");
380         return ( 0, $self->loc('Permission Denied') );
381     }
382
383     my $delegated_ace = RT::ACE->new( $self->CurrentUser );
384
385     # Make sure the right doesn't already exist.
386     $delegated_ace->LoadByCols( PrincipalId   => $princ_obj->Id,
387                                 PrincipalType => 'Group',
388                                 RightName     => $self->__Value('RightName'),
389                                 ObjectType    => $self->__Value('ObjectType'),
390                                 ObjectId      => $self->__Value('ObjectId'),
391                                 DelegatedBy => $self->CurrentUser->PrincipalId,
392                                 DelegatedFrom => $self->id );
393     if ( $delegated_ace->Id ) {
394         return ( 0, $self->loc('That principal already has that right') );
395     }
396     my $id = $delegated_ace->SUPER::Create(
397         PrincipalId   => $princ_obj->Id,
398         PrincipalType => 'Group',          # do we want to hardcode this?
399         RightName     => $self->__Value('RightName'),
400         ObjectType    => $self->__Value('ObjectType'),
401         ObjectId      => $self->__Value('ObjectId'),
402         DelegatedBy   => $self->CurrentUser->PrincipalId,
403         DelegatedFrom => $self->id );
404
405     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
406     # TODO what about the groups key cache?
407     RT::Principal->InvalidateACLCache();
408
409     if ( $id > 0 ) {
410         return ( $id, $self->loc('Right Delegated') );
411     }
412     else {
413         return ( 0, $self->loc('System error. Right not delegated.') );
414     }
415 }
416
417 # }}}
418
419 # {{{ sub Delete 
420
421 =head2 Delete { InsideTransaction => undef}
422
423 Delete this object. This method should ONLY ever be called from RT::User or RT::Group (or from itself)
424 If this is being called from within a transaction, specify a true value for the parameter InsideTransaction.
425 Really, DBIx::SearchBuilder should use and/or fake subtransactions
426
427 This routine will also recurse and delete any delegations of this right
428
429 =cut
430
431 sub Delete {
432     my $self = shift;
433
434     unless ( $self->Id ) {
435         return ( 0, $self->loc('Right not loaded.') );
436     }
437
438     # A user can delete an ACE if the current user has the right to modify it and it's not a delegated ACE
439     # or if it's a delegated ACE and it was delegated by the current user
440     unless (
441          (    $self->CurrentUser->HasRight(Right => 'ModifyACL', Object => $self->Object)
442            && $self->__Value('DelegatedBy') == 0 )
443          || ( $self->__Value('DelegatedBy') == $self->CurrentUser->PrincipalId )
444       ) {
445         return ( 0, $self->loc('Permission Denied') );
446     }
447     $self->_Delete(@_);
448 }
449
450 # Helper for Delete with no ACL check
451 sub _Delete {
452     my $self = shift;
453     my %args = ( InsideTransaction => undef,
454                  @_ );
455
456     my $InsideTransaction = $args{'InsideTransaction'};
457
458     $RT::Handle->BeginTransaction() unless $InsideTransaction;
459
460     my $delegated_from_this = RT::ACL->new($RT::SystemUser);
461     $delegated_from_this->Limit( FIELD    => 'DelegatedFrom',
462                                  OPERATOR => '=',
463                                  VALUE    => $self->Id );
464
465     my $delete_succeeded = 1;
466     my $submsg;
467     while ( my $delegated_ace = $delegated_from_this->Next ) {
468         ( $delete_succeeded, $submsg ) =
469           $delegated_ace->_Delete( InsideTransaction => 1 );
470         last unless ($delete_succeeded);
471     }
472
473     unless ($delete_succeeded) {
474         $RT::Handle->Rollback() unless $InsideTransaction;
475         return ( 0, $self->loc('Right could not be revoked') );
476     }
477
478     my ( $val, $msg ) = $self->SUPER::Delete(@_);
479
480     # If we're revoking delegation rights (see above), we may need to
481     # revoke all rights delegated by the recipient.
482     if ($val and ($self->RightName() eq 'DelegateRights' or
483                   $self->RightName() eq 'SuperUser')) {
484         $val = $self->PrincipalObj->CleanupInvalidDelegations( InsideTransaction => 1 );
485     }
486
487     if ($val) {
488         #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
489         # TODO what about the groups key cache?
490         RT::Principal->InvalidateACLCache();
491         $RT::Handle->Commit() unless $InsideTransaction;
492         return ( $val, $self->loc('Right revoked') );
493     }
494
495     $RT::Handle->Rollback() unless $InsideTransaction;
496     return ( 0, $self->loc('Right could not be revoked') );
497 }
498
499 # }}}
500
501 # {{{ sub _BootstrapCreate 
502
503 =head2 _BootstrapCreate
504
505 Grant a right with no error checking and no ACL. this is _only_ for 
506 installation. If you use this routine without the author's explicit 
507 written approval, he will hunt you down and make you spend eternity
508 translating mozilla's code into FORTRAN or intercal.
509
510 If you think you need this routine, you've mistaken. 
511
512 =cut
513
514 sub _BootstrapCreate {
515     my $self = shift;
516     my %args = (@_);
517
518     # When bootstrapping, make sure we get the _right_ users
519     if ( $args{'UserId'} ) {
520         my $user = RT::User->new( $self->CurrentUser );
521         $user->Load( $args{'UserId'} );
522         delete $args{'UserId'};
523         $args{'PrincipalId'}   = $user->PrincipalId;
524         $args{'PrincipalType'} = 'User';
525     }
526
527     my $id = $self->SUPER::Create(%args);
528
529     if ( $id > 0 ) {
530         return ($id);
531     }
532     else {
533         $RT::Logger->err('System error. right not granted.');
534         return (undef);
535     }
536
537 }
538
539 # }}}
540
541 # {{{ sub CanonicalizeRightName
542
543 sub RightName {
544     my $self = shift;
545     my $val = $self->_Value('RightName');
546     return $val unless $val;
547
548     my $available = $self->Object->AvailableRights;
549     foreach my $right ( keys %$available ) {
550         return $right if $val eq $self->CanonicalizeRightName($right);
551     }
552
553     $RT::Logger->error("Invalid right. Couldn't canonicalize right '$val'");
554     return $val;
555 }
556
557 =head2 CanonicalizeRightName <RIGHT>
558
559 Takes a queue or system right name in any case and returns it in
560 the correct case. If it's not found, will return undef.
561
562 =cut
563
564 sub CanonicalizeRightName {
565     my $self  = shift;
566     return $LOWERCASERIGHTNAMES{ lc shift };
567 }
568
569 # }}}
570
571
572 # {{{ sub Object
573
574 =head2 Object
575
576 If the object this ACE applies to is a queue, returns the queue object. 
577 If the object this ACE applies to is a group, returns the group object. 
578 If it's the system object, returns undef. 
579
580 If the user has no rights, returns undef.
581
582 =cut
583
584
585
586
587 sub Object {
588     my $self = shift;
589
590     my $appliesto_obj;
591
592     if ($self->__Value('ObjectType') && $OBJECT_TYPES{$self->__Value('ObjectType')} ) {
593         $appliesto_obj =  $self->__Value('ObjectType')->new($self->CurrentUser);
594         unless (ref( $appliesto_obj) eq $self->__Value('ObjectType')) {
595             return undef;
596         }
597         $appliesto_obj->Load( $self->__Value('ObjectId') );
598         return ($appliesto_obj);
599      }
600     else {
601         $RT::Logger->warning( "$self -> Object called for an object "
602                               . "of an unknown type:"
603                               . $self->__Value('ObjectType') );
604         return (undef);
605     }
606 }
607
608 # }}}
609
610 # {{{ sub PrincipalObj
611
612 =head2 PrincipalObj
613
614 Returns the RT::Principal object for this ACE. 
615
616 =cut
617
618 sub PrincipalObj {
619     my $self = shift;
620
621     my $princ_obj = RT::Principal->new( $self->CurrentUser );
622     $princ_obj->Load( $self->__Value('PrincipalId') );
623
624     unless ( $princ_obj->Id ) {
625         $RT::Logger->err(
626                    "ACE " . $self->Id . " couldn't load its principal object" );
627     }
628     return ($princ_obj);
629
630 }
631
632 # }}}
633
634 # {{{ ACL related methods
635
636 # {{{ sub _Set
637
638 sub _Set {
639     my $self = shift;
640     return ( 0, $self->loc("ACEs can only be created and deleted.") );
641 }
642
643 # }}}
644
645 # {{{ sub _Value
646
647 sub _Value {
648     my $self = shift;
649
650     if ( $self->__Value('DelegatedBy') eq $self->CurrentUser->PrincipalId ) {
651         return ( $self->__Value(@_) );
652     }
653     elsif ( $self->PrincipalObj->IsGroup
654             && $self->PrincipalObj->Object->HasMemberRecursively(
655                                                 $self->CurrentUser->PrincipalObj
656             )
657       ) {
658         return ( $self->__Value(@_) );
659     }
660     elsif ( $self->CurrentUser->HasRight(Right => 'ShowACL', Object => $self->Object) ) {
661         return ( $self->__Value(@_) );
662     }
663     else {
664         return undef;
665     }
666 }
667
668 # }}}
669
670
671 # }}}
672
673 # {{{ _CanonicalizePrincipal 
674
675 =head2 _CanonicalizePrincipal (PrincipalId, PrincipalType)
676
677 Takes a principal id and a principal type.
678
679 If the principal is a user, resolves it to the proper acl equivalence group.
680 Returns a tuple of  (RT::Principal, PrincipalType)  for the principal we really want to work with
681
682 =cut
683
684 sub _CanonicalizePrincipal {
685     my $self       = shift;
686     my $princ_id   = shift;
687     my $princ_type = shift || '';
688
689     my $princ_obj = RT::Principal->new($RT::SystemUser);
690     $princ_obj->Load($princ_id);
691
692     unless ( $princ_obj->Id ) {
693         use Carp;
694         $RT::Logger->crit(Carp::longmess);
695         $RT::Logger->crit("Can't load a principal for id $princ_id");
696         return ( $princ_obj, undef );
697     }
698
699     # Rights never get granted to users. they get granted to their 
700     # ACL equivalence groups
701     if ( $princ_type eq 'User' ) {
702         my $equiv_group = RT::Group->new( $self->CurrentUser );
703         $equiv_group->LoadACLEquivalenceGroup($princ_obj);
704         unless ( $equiv_group->Id ) {
705             $RT::Logger->crit( "No ACL equiv group for princ " . $princ_obj->id );
706             return ( RT::Principal->new($RT::SystemUser), undef );
707         }
708         $princ_obj  = $equiv_group->PrincipalObj();
709         $princ_type = 'Group';
710
711     }
712     return ( $princ_obj, $princ_type );
713 }
714
715 sub _ParseObjectArg {
716     my $self = shift;
717     my %args = ( Object    => undef,
718                  ObjectId    => undef,
719                  ObjectType    => undef,
720                  @_ );
721
722     if( $args{'Object'} && ($args{'ObjectId'} || $args{'ObjectType'}) ) {
723         $RT::Logger->crit( "Method called with an ObjectType or an ObjectId and Object args" );
724         return ();
725     } elsif( $args{'Object'} && ref($args{'Object'}) &&  !$args{'Object'}->can('id') ) {
726         $RT::Logger->crit( "Method called called Object that has no id method" );
727         return ();
728     } elsif( $args{'Object'} ) {
729         my $obj = $args{'Object'};
730         return ($obj, ref $obj, $obj->id);
731     } elsif ( $args{'ObjectType'} ) {
732         my $obj =  $args{'ObjectType'}->new( $self->CurrentUser );
733         $obj->Load( $args{'ObjectId'} );
734         return ($obj, ref $obj, $obj->id);
735     } else {
736         $RT::Logger->crit( "Method called with wrong args" );
737         return ();
738     }
739 }
740
741
742 # }}}
743 1;