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