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