import rt 3.6.4
[freeside.git] / rt / lib / RT / Group_Overlay.pm
1
2 # BEGIN BPS TAGGED BLOCK {{{
3
4 # COPYRIGHT:
5 #  
6 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
7 #                                          <jesse@bestpractical.com>
8
9 # (Except where explicitly superseded by other copyright notices)
10
11
12 # LICENSE:
13
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/copyleft/gpl.html.
29
30
31 # CONTRIBUTION SUBMISSION POLICY:
32
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47
48 # END BPS TAGGED BLOCK }}}
49 # Released under the terms of version 2 of the GNU Public License
50
51 =head1 NAME
52
53   RT::Group - RT\'s group object
54
55 =head1 SYNOPSIS
56
57 use RT::Group;
58 my $group = new RT::Group($CurrentUser);
59
60 =head1 DESCRIPTION
61
62 An RT group object.
63
64 =head1 METHODS
65
66
67 =begin testing
68
69 # {{{ Tests
70 ok (require RT::Group);
71
72 ok (my $group = RT::Group->new($RT::SystemUser), "instantiated a group object");
73 ok (my ($id, $msg) = $group->CreateUserDefinedGroup( Name => 'TestGroup', Description => 'A test group',
74                     ), 'Created a new group');
75 ok ($id != 0, "Group id is $id");
76 ok ($group->Name eq 'TestGroup', "The group's name is 'TestGroup'");
77 my $ng = RT::Group->new($RT::SystemUser);
78
79 ok($ng->LoadUserDefinedGroup('TestGroup'), "Loaded testgroup");
80 ok(($ng->id == $group->id), "Loaded the right group");
81
82
83 ok (($id,$msg) = $ng->AddMember('1'), "Added a member to the group");
84 ok($id, $msg);
85 ok (($id,$msg) = $ng->AddMember('2' ), "Added a member to the group");
86 ok($id, $msg);
87 ok (($id,$msg) = $ng->AddMember('3' ), "Added a member to the group");
88 ok($id, $msg);
89
90 # Group 1 now has members 1, 2 ,3
91
92 my $group_2 = RT::Group->new($RT::SystemUser);
93 ok (my ($id_2, $msg_2) = $group_2->CreateUserDefinedGroup( Name => 'TestGroup2', Description => 'A second test group'), , 'Created a new group');
94 ok ($id_2 != 0, "Created group 2 ok- $msg_2 ");
95 ok (($id,$msg) = $group_2->AddMember($ng->PrincipalId), "Made TestGroup a member of testgroup2");
96 ok($id, $msg);
97 ok (($id,$msg) = $group_2->AddMember('1' ), "Added  member RT_System to the group TestGroup2");
98 ok($id, $msg);
99
100 # Group 2 how has 1, g1->{1, 2,3}
101
102 my $group_3 = RT::Group->new($RT::SystemUser);
103 ok (($id_3, $msg) = $group_3->CreateUserDefinedGroup( Name => 'TestGroup3', Description => 'A second test group'), 'Created a new group');
104 ok ($id_3 != 0, "Created group 3 ok - $msg");
105 ok (($id,$msg) =$group_3->AddMember($group_2->PrincipalId), "Made TestGroup a member of testgroup2");
106 ok($id, $msg);
107
108 # g3 now has g2->{1, g1->{1,2,3}}
109
110 my $principal_1 = RT::Principal->new($RT::SystemUser);
111 $principal_1->Load('1');
112
113 my $principal_2 = RT::Principal->new($RT::SystemUser);
114 $principal_2->Load('2');
115
116 ok (($id,$msg) = $group_3->AddMember('1' ), "Added  member RT_System to the group TestGroup2");
117 ok($id, $msg);
118
119 # g3 now has 1, g2->{1, g1->{1,2,3}}
120
121 ok($group_3->HasMember($principal_2) == undef, "group 3 doesn't have member 2");
122 ok($group_3->HasMemberRecursively($principal_2), "group 3 has member 2 recursively");
123 ok($ng->HasMember($principal_2) , "group ".$ng->Id." has member 2");
124 my ($delid , $delmsg) =$ng->DeleteMember($principal_2->Id);
125 ok ($delid !=0, "Sucessfully deleted it-".$delid."-".$delmsg);
126
127 #Gotta reload the group objects, since we've been messing with various internals.
128 # we shouldn't need to do this.
129 #$ng->LoadUserDefinedGroup('TestGroup');
130 #$group_2->LoadUserDefinedGroup('TestGroup2');
131 #$group_3->LoadUserDefinedGroup('TestGroup');
132
133 # G1 now has 1, 3
134 # Group 2 how has 1, g1->{1, 3}
135 # g3 now has  1, g2->{1, g1->{1, 3}}
136
137 ok(!$ng->HasMember($principal_2)  , "group ".$ng->Id." no longer has member 2");
138 ok($group_3->HasMemberRecursively($principal_2) == undef, "group 3 doesn't have member 2");
139 ok($group_2->HasMemberRecursively($principal_2) == undef, "group 2 doesn't have member 2");
140 ok($ng->HasMember($principal_2) == undef, "group 1 doesn't have member 2");;
141 ok($group_3->HasMemberRecursively($principal_2) == undef, "group 3 has member 2 recursively");
142
143 # }}}
144
145 =end testing
146
147
148
149 =cut
150
151
152 package RT::Group;
153
154 use strict;
155 no warnings qw(redefine);
156
157 use RT::Users;
158 use RT::GroupMembers;
159 use RT::Principals;
160 use RT::ACL;
161
162 use vars qw/$RIGHTS/;
163
164 $RIGHTS = {
165     AdminGroup           => 'Modify group metadata or delete group',  # loc_pair
166     AdminGroupMembership =>
167       'Modify membership roster for this group',                      # loc_pair
168     ModifyOwnMembership => 'Join or leave this group',                 # loc_pair
169     EditSavedSearches => 'Edit saved searches for this group',        # loc_pair
170     ShowSavedSearches => 'Display saved searches for this group',        # loc_pair
171     SeeGroup => 'Make this group visible to user',                    # loc_pair
172 };
173
174 # Tell RT::ACE that this sort of object can get acls granted
175 $RT::ACE::OBJECT_TYPES{'RT::Group'} = 1;
176
177
178 #
179
180 # TODO: This should be refactored out into an RT::ACLedObject or something
181 # stuff the rights into a hash of rights that can exist.
182
183 foreach my $right ( keys %{$RIGHTS} ) {
184     $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
185 }
186
187
188 =head2 AvailableRights
189
190 Returns a hash of available rights for this object. The keys are the right names and the values are a description of what the rights do
191
192 =cut
193
194 sub AvailableRights {
195     my $self = shift;
196     return($RIGHTS);
197 }
198
199
200 # {{{ sub SelfDescription
201
202 =head2 SelfDescription
203
204 Returns a user-readable description of what this group is for and what it's named.
205
206 =cut
207
208 sub SelfDescription {
209         my $self = shift;
210         if ($self->Domain eq 'ACLEquivalence') {
211                 my $user = RT::Principal->new($self->CurrentUser);
212                 $user->Load($self->Instance);
213                 return $self->loc("user [_1]",$user->Object->Name);
214         }
215         elsif ($self->Domain eq 'UserDefined') {
216                 return $self->loc("group '[_1]'",$self->Name);
217         }
218         elsif ($self->Domain eq 'Personal') {
219                 my $user = RT::User->new($self->CurrentUser);
220                 $user->Load($self->Instance);
221                 return $self->loc("personal group '[_1]' for user '[_2]'",$self->Name, $user->Name);
222         }
223         elsif ($self->Domain eq 'RT::System-Role') {
224                 return $self->loc("system [_1]",$self->Type);
225         }
226         elsif ($self->Domain eq 'RT::Queue-Role') {
227                 my $queue = RT::Queue->new($self->CurrentUser);
228                 $queue->Load($self->Instance);
229                 return $self->loc("queue [_1] [_2]",$queue->Name, $self->Type);
230         }
231         elsif ($self->Domain eq 'RT::Ticket-Role') {
232                 return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Type);
233         }
234         elsif ($self->Domain eq 'SystemInternal') {
235                 return $self->loc("system group '[_1]'",$self->Type);
236         }
237         else {
238                 return $self->loc("undescribed group [_1]",$self->Id);
239         }
240 }
241
242 # }}}
243
244 # {{{ sub Load 
245
246 =head2 Load ID
247
248 Load a group object from the database. Takes a single argument.
249 If the argument is numerical, load by the column 'id'. Otherwise, 
250 complain and return.
251
252 =cut
253
254 sub Load {
255     my $self       = shift;
256     my $identifier = shift || return undef;
257
258     #if it's an int, load by id. otherwise, load by name.
259     if ( $identifier !~ /\D/ ) {
260         $self->SUPER::LoadById($identifier);
261     }
262     else {
263         $RT::Logger->crit("Group -> Load called with a bogus argument");
264         return undef;
265     }
266 }
267
268 # }}}
269
270 # {{{ sub LoadUserDefinedGroup 
271
272 =head2 LoadUserDefinedGroup NAME
273
274 Loads a system group from the database. The only argument is
275 the group's name.
276
277
278 =cut
279
280 sub LoadUserDefinedGroup {
281     my $self       = shift;
282     my $identifier = shift;
283
284         $self->LoadByCols( "Domain" => 'UserDefined',
285                            "Name" => $identifier );
286 }
287
288 # }}}
289
290 # {{{ sub LoadACLEquivalenceGroup 
291
292 =head2 LoadACLEquivalenceGroup  PRINCIPAL
293
294 Loads a user's acl equivalence group. Takes a principal object.
295 ACL equivalnce groups are used to simplify the acl system. Each user
296 has one group that only he is a member of. Rights granted to the user
297 are actually granted to that group. This greatly simplifies ACL checks.
298 While this results in a somewhat more complex setup when creating users
299 and granting ACLs, it _greatly_ simplifies acl checks.
300
301
302
303 =cut
304
305 sub LoadACLEquivalenceGroup {
306     my $self       = shift;
307     my $princ = shift;
308
309         $self->LoadByCols( "Domain" => 'ACLEquivalence',
310                             "Type" => 'UserEquiv',
311                            "Instance" => $princ->Id);
312 }
313
314 # }}}
315
316 # {{{ sub LoadPersonalGroup 
317
318 =head2 LoadPersonalGroup {Name => NAME, User => USERID}
319
320 Loads a personal group from the database. 
321
322 =cut
323
324 sub LoadPersonalGroup {
325     my $self       = shift;
326     my %args =  (   Name => undef,
327                     User => undef,
328                     @_);
329
330         $self->LoadByCols( "Domain" => 'Personal',
331                            "Instance" => $args{'User'},
332                            "Type" => '',
333                            "Name" => $args{'Name'} );
334 }
335
336 # }}}
337
338 # {{{ sub LoadSystemInternalGroup 
339
340 =head2 LoadSystemInternalGroup NAME
341
342 Loads a Pseudo group from the database. The only argument is
343 the group's name.
344
345
346 =cut
347
348 sub LoadSystemInternalGroup {
349     my $self       = shift;
350     my $identifier = shift;
351
352         $self->LoadByCols( "Domain" => 'SystemInternal',
353                            "Type" => $identifier );
354 }
355
356 # }}}
357
358 # {{{ sub LoadTicketRoleGroup 
359
360 =head2 LoadTicketRoleGroup  { Ticket => TICKET_ID, Type => TYPE }
361
362 Loads a ticket group from the database. 
363
364 Takes a param hash with 2 parameters:
365
366     Ticket is the TicketId we're curious about
367     Type is the type of Group we're trying to load: 
368         Requestor, Cc, AdminCc, Owner
369
370 =cut
371
372 sub LoadTicketRoleGroup {
373     my $self       = shift;
374     my %args = (Ticket => '0',
375                 Type => undef,
376                 @_);
377         $self->LoadByCols( Domain => 'RT::Ticket-Role',
378                            Instance =>$args{'Ticket'}, 
379                            Type => $args{'Type'}
380                            );
381 }
382
383 # }}}
384
385 # {{{ sub LoadQueueRoleGroup 
386
387 =head2 LoadQueueRoleGroup  { Queue => Queue_ID, Type => TYPE }
388
389 Loads a Queue group from the database. 
390
391 Takes a param hash with 2 parameters:
392
393     Queue is the QueueId we're curious about
394     Type is the type of Group we're trying to load: 
395         Requestor, Cc, AdminCc, Owner
396
397 =cut
398
399 sub LoadQueueRoleGroup {
400     my $self       = shift;
401     my %args = (Queue => undef,
402                 Type => undef,
403                 @_);
404         $self->LoadByCols( Domain => 'RT::Queue-Role',
405                            Instance =>$args{'Queue'}, 
406                            Type => $args{'Type'}
407                            );
408 }
409
410 # }}}
411
412 # {{{ sub LoadSystemRoleGroup 
413
414 =head2 LoadSystemRoleGroup  Type
415
416 Loads a System group from the database. 
417
418 Takes a single param: Type
419
420     Type is the type of Group we're trying to load: 
421         Requestor, Cc, AdminCc, Owner
422
423 =cut
424
425 sub LoadSystemRoleGroup {
426     my $self       = shift;
427     my $type = shift;
428         $self->LoadByCols( Domain => 'RT::System-Role',
429                            Type => $type
430                            );
431 }
432
433 # }}}
434
435 # {{{ sub Create
436
437 =head2 Create
438
439 You need to specify what sort of group you're creating by calling one of the other
440 Create_____ routines.
441
442 =cut
443
444 sub Create {
445     my $self = shift;
446     $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
447     return(0,$self->loc('Permission Denied'));
448 }
449
450 # }}}
451
452 # {{{ sub _Create
453
454 =head2 _Create
455
456 Takes a paramhash with named arguments: Name, Description.
457
458 Returns a tuple of (Id, Message).  If id is 0, the create failed
459
460 =cut
461
462 sub _Create {
463     my $self = shift;
464     my %args = (
465         Name        => undef,
466         Description => undef,
467         Domain      => undef,
468         Type        => undef,
469         Instance    => '0',
470         InsideTransaction => undef,
471         _RecordTransaction => 1,
472         @_
473     );
474
475     $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
476     # Groups deal with principal ids, rather than user ids.
477     # When creating this group, set up a principal Id for it.
478     my $principal    = RT::Principal->new( $self->CurrentUser );
479     my $principal_id = $principal->Create(
480         PrincipalType => 'Group',
481         ObjectId      => '0'
482     );
483     $principal->__Set(Field => 'ObjectId', Value => $principal_id);
484
485
486     $self->SUPER::Create(
487         id          => $principal_id,
488         Name        => $args{'Name'},
489         Description => $args{'Description'},
490         Type        => $args{'Type'},
491         Domain      => $args{'Domain'},
492         Instance    => ($args{'Instance'} || '0')
493     );
494     my $id = $self->Id;
495     unless ($id) {
496         return ( 0, $self->loc('Could not create group') );
497     }
498
499     # If we couldn't create a principal Id, get the fuck out.
500     unless ($principal_id) {
501         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
502         $RT::Logger->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
503         return ( 0, $self->loc('Could not create group') );
504     }
505
506     # Now we make the group a member of itself as a cached group member
507     # this needs to exist so that group ACL checks don't fall over.
508     # you're checking CachedGroupMembers to see if the principal in question
509     # is a member of the principal the rights have been granted too
510
511     # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as 
512     # cached members. thankfully, we're creating the group now...so it has no members.
513     my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
514     $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
515
516
517     if ( $args{'_RecordTransaction'} ) {
518         $self->_NewTransaction( Type => "Create" );
519     }
520
521     $RT::Handle->Commit() unless ($args{'InsideTransaction'});
522
523     return ( $id, $self->loc("Group created") );
524 }
525
526 # }}}
527
528 # {{{ CreateUserDefinedGroup
529
530 =head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
531
532 A helper subroutine which creates a system group 
533
534 Returns a tuple of (Id, Message).  If id is 0, the create failed
535
536 =cut
537
538 sub CreateUserDefinedGroup {
539     my $self = shift;
540
541     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
542         $RT::Logger->warning( $self->CurrentUser->Name
543               . " Tried to create a group without permission." );
544         return ( 0, $self->loc('Permission Denied') );
545     }
546
547     return($self->_Create( Domain => 'UserDefined', Type => '', Instance => '', @_));
548 }
549
550 # }}}
551
552 # {{{ _CreateACLEquivalenceGroup
553
554 =head2 _CreateACLEquivalenceGroup { Principal }
555
556 A helper subroutine which creates a group containing only 
557 an individual user. This gets used by the ACL system to check rights.
558 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
559
560 Returns a tuple of (Id, Message).  If id is 0, the create failed
561
562 =cut
563
564 sub _CreateACLEquivalenceGroup { 
565     my $self = shift;
566     my $princ = shift;
567  
568       my $id = $self->_Create( Domain => 'ACLEquivalence', 
569                            Type => 'UserEquiv',
570                            Name => 'User '. $princ->Object->Id,
571                            Description => 'ACL equiv. for user '.$princ->Object->Id,
572                            Instance => $princ->Id,
573                            InsideTransaction => 1);
574       unless ($id) {
575         $RT::Logger->crit("Couldn't create ACL equivalence group");
576         return undef;
577       }
578     
579        # We use stashuser so we don't get transactions inside transactions
580        # and so we bypass all sorts of cruft we don't need
581        my $aclstash = RT::GroupMember->new($self->CurrentUser);
582        my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
583                                              Member => $princ);
584
585       unless ($stash_id) {
586         $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
587         # We call super delete so we don't get acl checked.
588         $self->SUPER::Delete();
589         return(undef);
590       }
591     return ($id);
592 }
593
594 # }}}
595
596 # {{{ CreatePersonalGroup
597
598 =head2 CreatePersonalGroup { PrincipalId => PRINCIPAL_ID, Name => "name", Description => "Description"}
599
600 A helper subroutine which creates a personal group. Generally,
601 personal groups are used for ACL delegation and adding to ticket roles
602 PrincipalId defaults to the current user's principal id.
603
604 Returns a tuple of (Id, Message).  If id is 0, the create failed
605
606 =cut
607
608 sub CreatePersonalGroup {
609     my $self = shift;
610     my %args = (
611         Name        => undef,
612         Description => undef,
613         PrincipalId => $self->CurrentUser->PrincipalId,
614         @_
615     );
616
617     if ( $self->CurrentUser->PrincipalId == $args{'PrincipalId'} ) {
618
619         unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups') ) {
620             $RT::Logger->warning( $self->CurrentUser->Name
621                   . " Tried to create a group without permission." );
622             return ( 0, $self->loc('Permission Denied') );
623         }
624
625     }
626     else {
627         unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
628             $RT::Logger->warning( $self->CurrentUser->Name
629                   . " Tried to create a group without permission." );
630             return ( 0, $self->loc('Permission Denied') );
631         }
632
633     }
634
635     return (
636         $self->_Create(
637             Domain      => 'Personal',
638             Type        => '',
639             Instance    => $args{'PrincipalId'},
640             Name        => $args{'Name'},
641             Description => $args{'Description'}
642         )
643     );
644 }
645
646 # }}}
647
648 # {{{ CreateRoleGroup 
649
650 =head2 CreateRoleGroup { Domain => DOMAIN, Type =>  TYPE, Instance => ID }
651
652 A helper subroutine which creates a  ticket group. (What RT 2.0 called Ticket watchers)
653 Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner") 
654 Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
655 Instance is the id of the ticket or queue in question
656
657 This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
658
659 Returns a tuple of (Id, Message).  If id is 0, the create failed
660
661 =cut
662
663 sub CreateRoleGroup {
664     my $self = shift;
665     my %args = ( Instance => undef,
666                  Type     => undef,
667                  Domain   => undef,
668                  @_ );
669     unless ( $args{'Type'} =~ /^(?:Cc|AdminCc|Requestor|Owner)$/ ) {
670         return ( 0, $self->loc("Invalid Group Type") );
671     }
672
673
674     return ( $self->_Create( Domain            => $args{'Domain'},
675                              Instance          => $args{'Instance'},
676                              Type              => $args{'Type'},
677                              InsideTransaction => 1 ) );
678 }
679
680 # }}}
681
682 # {{{ sub Delete
683
684 =head2 Delete
685
686 Delete this object
687
688 =cut
689
690 sub Delete {
691     my $self = shift;
692
693     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
694         return ( 0, 'Permission Denied' );
695     }
696
697     $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
698     # TODO XXX 
699    
700     # Remove the principal object
701     # Remove this group from anything it's a member of.
702     # Remove all cached members of this group
703     # Remove any rights granted to this group
704     # remove any rights delegated by way of this group
705
706     return ( $self->SUPER::Delete(@_) );
707 }
708
709 # }}}
710
711 =head2 SetDisabled BOOL
712
713 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
714 It will not appear in most group listings.
715
716 This routine finds all the cached group members that are members of this group  (recursively) and disables them.
717
718 =cut 
719
720  # }}}
721
722  sub SetDisabled {
723      my $self = shift;
724      my $val = shift;
725     if ($self->Domain eq 'Personal') {
726                 if ($self->CurrentUser->PrincipalId == $self->Instance) {
727                 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
728                         return ( 0, $self->loc('Permission Denied') );
729                 }
730         } else {
731                 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
732                          return ( 0, $self->loc('Permission Denied') );
733                 }
734         }
735         }
736         else {
737         unless ( $self->CurrentUserHasRight('AdminGroup') ) {
738                  return (0, $self->loc('Permission Denied'));
739     }
740     }
741     $RT::Handle->BeginTransaction();
742     $self->PrincipalObj->SetDisabled($val);
743
744
745
746
747     # Find all occurrences of this member as a member of this group
748     # in the cache and nuke them, recursively.
749
750     # The following code will delete all Cached Group members
751     # where this member's group is _not_ the primary group 
752     # (Ie if we're deleting C as a member of B, and B happens to be 
753     # a member of A, will delete C as a member of A without touching
754     # C as a member of B
755
756     my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
757
758     $cached_submembers->Limit( FIELD    => 'ImmediateParentId', OPERATOR => '=', VALUE    => $self->Id);
759
760     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
761     # TODO what about the groups key cache?
762     RT::Principal->InvalidateACLCache();
763
764
765
766     while ( my $item = $cached_submembers->Next() ) {
767         my $del_err = $item->SetDisabled($val);
768         unless ($del_err) {
769             $RT::Handle->Rollback();
770             $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
771             return (undef);
772         }
773     }
774
775     $RT::Handle->Commit();
776     return (1, $self->loc("Succeeded"));
777
778 }
779
780 # }}}
781
782
783
784 sub Disabled {
785     my $self = shift;
786     $self->PrincipalObj->Disabled(@_);
787 }
788
789
790 # {{{ DeepMembersObj
791
792 =head2 DeepMembersObj
793
794 Returns an RT::CachedGroupMembers object of this group's members,
795 including all members of subgroups.
796
797 =cut
798
799 sub DeepMembersObj {
800     my $self = shift;
801     my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
802
803     #If we don't have rights, don't include any results
804     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
805     $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
806
807     return ( $members_obj );
808
809 }
810
811 # }}}
812
813 # {{{ UserMembersObj
814
815 =head2 UserMembersObj
816
817 Returns an RT::Users object of this group's members, including
818 all members of subgroups
819
820 =cut
821
822 sub UserMembersObj {
823     my $self = shift;
824
825     my $users = RT::Users->new($self->CurrentUser);
826
827     #If we don't have rights, don't include any results
828     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
829
830     my $cached_members = $users->NewAlias('CachedGroupMembers');
831     $users->Join(ALIAS1 => $cached_members, FIELD1 => 'MemberId',
832                  ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id');
833     $users->Limit(ALIAS => $cached_members, 
834                   FIELD => 'GroupId',
835                   OPERATOR => '=',
836                   VALUE => $self->PrincipalId);
837
838     return ( $users);
839
840 }
841
842 # }}}
843
844 # {{{ MembersObj
845
846 =head2 MembersObj
847
848 Returns an RT::GroupMembers object of this group's direct members.
849
850 =cut
851
852 sub MembersObj {
853     my $self = shift;
854     my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
855
856     #If we don't have rights, don't include any results
857     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
858     $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
859
860     return ( $members_obj );
861
862 }
863
864 # }}}
865
866 # {{{ MemberEmailAddresses
867
868 =head2 MemberEmailAddresses
869
870 Returns an array of the email addresses of all of this group's members
871
872
873 =cut
874
875 sub MemberEmailAddresses {
876     my $self = shift;
877
878     my %addresses;
879     my $members = $self->UserMembersObj();
880     while (my $member = $members->Next) {
881         $addresses{$member->EmailAddress} = 1;
882     }
883     return(sort keys %addresses);
884 }
885
886 # }}}
887
888 # {{{ MemberEmailAddressesAsString
889
890 =head2 MemberEmailAddressesAsString
891
892 Returns a comma delimited string of the email addresses of all users 
893 who are members of this group.
894
895 =cut
896
897
898 sub MemberEmailAddressesAsString {
899     my $self = shift;
900     return (join(', ', $self->MemberEmailAddresses));
901 }
902
903 # }}}
904
905 # {{{ AddMember
906
907 =head2 AddMember PRINCIPAL_ID
908
909 AddMember adds a principal to this group.  It takes a single principal id.
910 Returns a two value array. the first value is true on successful 
911 addition or 0 on failure.  The second value is a textual status msg.
912
913 =cut
914
915 sub AddMember {
916     my $self       = shift;
917     my $new_member = shift;
918
919
920
921     if ($self->Domain eq 'Personal') {
922                 if ($self->CurrentUser->PrincipalId == $self->Instance) {
923                 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
924                         return ( 0, $self->loc('Permission Denied') );
925                 }
926         } else {
927                 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
928                          return ( 0, $self->loc('Permission Denied') );
929                 }
930         }
931         }
932         
933         else {  
934     # We should only allow membership changes if the user has the right 
935     # to modify group membership or the user is the principal in question
936     # and the user has the right to modify his own membership
937     unless ( ($new_member == $self->CurrentUser->PrincipalId &&
938               $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
939               $self->CurrentUserHasRight('AdminGroupMembership') ) {
940         #User has no permission to be doing this
941         return ( 0, $self->loc("Permission Denied") );
942     }
943
944         } 
945     $self->_AddMember(PrincipalId => $new_member);
946 }
947
948 # A helper subroutine for AddMember that bypasses the ACL checks
949 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
950 # when we want to deal with groups according to queue rights
951 # In the dim future, this will all get factored out and life
952 # will get better       
953
954 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
955
956 sub _AddMember {
957     my $self = shift;
958     my %args = ( PrincipalId => undef,
959                  InsideTransaction => undef,
960                  @_);
961     my $new_member = $args{'PrincipalId'};
962
963     unless ($self->Id) {
964         $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
965         return(0, $self->loc("Group not found"));
966     }
967
968     unless ($new_member =~ /^\d+$/) {
969         $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
970     }
971
972
973     my $new_member_obj = RT::Principal->new( $self->CurrentUser );
974     $new_member_obj->Load($new_member);
975
976
977     unless ( $new_member_obj->Id ) {
978         $RT::Logger->debug("Couldn't find that principal");
979         return ( 0, $self->loc("Couldn't find that principal") );
980     }
981
982     if ( $self->HasMember( $new_member_obj ) ) {
983
984         #User is already a member of this group. no need to add it
985         return ( 0, $self->loc("Group already has member") );
986     }
987     if ( $new_member_obj->IsGroup &&
988          $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
989
990         #This group can't be made to be a member of itself
991         return ( 0, $self->loc("Groups can't be members of their members"));
992     }
993
994
995     my $member_object = RT::GroupMember->new( $self->CurrentUser );
996     my $id = $member_object->Create(
997         Member => $new_member_obj,
998         Group => $self->PrincipalObj,
999         InsideTransaction => $args{'InsideTransaction'}
1000     );
1001     if ($id) {
1002         return ( 1, $self->loc("Member added") );
1003     }
1004     else {
1005         return(0, $self->loc("Couldn't add member to group"));
1006     }
1007 }
1008 # }}}
1009
1010 # {{{ HasMember
1011
1012 =head2 HasMember RT::Principal
1013
1014 Takes an RT::Principal object returns a GroupMember Id if that user is a 
1015 member of this group.
1016 Returns undef if the user isn't a member of the group or if the current
1017 user doesn't have permission to find out. Arguably, it should differentiate
1018 between ACL failure and non membership.
1019
1020 =cut
1021
1022 sub HasMember {
1023     my $self    = shift;
1024     my $principal = shift;
1025
1026
1027     unless (UNIVERSAL::isa($principal,'RT::Principal')) {
1028         $RT::Logger->crit("Group::HasMember was called with an argument that".
1029                           "isn't an RT::Principal. It's $principal");
1030         return(undef);
1031     }
1032
1033     unless ($principal->Id) {
1034         return(undef);
1035     }
1036
1037     my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1038     $member_obj->LoadByCols( MemberId => $principal->id, 
1039                              GroupId => $self->PrincipalId );
1040
1041     #If we have a member object
1042     if ( defined $member_obj->id ) {
1043         return ( $member_obj->id );
1044     }
1045
1046     #If Load returns no objects, we have an undef id. 
1047     else {
1048         #$RT::Logger->debug($self." does not contain principal ".$principal->id);
1049         return (undef);
1050     }
1051 }
1052
1053 # }}}
1054
1055 # {{{ HasMemberRecursively
1056
1057 =head2 HasMemberRecursively RT::Principal
1058
1059 Takes an RT::Principal object and returns true if that user is a member of 
1060 this group.
1061 Returns undef if the user isn't a member of the group or if the current
1062 user doesn't have permission to find out. Arguably, it should differentiate
1063 between ACL failure and non membership.
1064
1065 =cut
1066
1067 sub HasMemberRecursively {
1068     my $self    = shift;
1069     my $principal = shift;
1070
1071     unless (UNIVERSAL::isa($principal,'RT::Principal')) {
1072         $RT::Logger->crit("Group::HasMemberRecursively was called with an argument that".
1073                           "isn't an RT::Principal. It's $principal");
1074         return(undef);
1075     }
1076     my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1077     $member_obj->LoadByCols( MemberId => $principal->Id,
1078                              GroupId => $self->PrincipalId ,
1079                              Disabled => 0
1080                              );
1081
1082     #If we have a member object
1083     if ( defined $member_obj->id ) {
1084         return ( 1);
1085     }
1086
1087     #If Load returns no objects, we have an undef id. 
1088     else {
1089         return (undef);
1090     }
1091 }
1092
1093 # }}}
1094
1095 # {{{ DeleteMember
1096
1097 =head2 DeleteMember PRINCIPAL_ID
1098
1099 Takes the principal id of a current user or group.
1100 If the current user has apropriate rights,
1101 removes that GroupMember from this group.
1102 Returns a two value array. the first value is true on successful 
1103 addition or 0 on failure.  The second value is a textual status msg.
1104
1105 =cut
1106
1107 sub DeleteMember {
1108     my $self   = shift;
1109     my $member_id = shift;
1110
1111
1112     # We should only allow membership changes if the user has the right 
1113     # to modify group membership or the user is the principal in question
1114     # and the user has the right to modify his own membership
1115
1116     if ($self->Domain eq 'Personal') {
1117                 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1118                 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1119                         return ( 0, $self->loc('Permission Denied') );
1120                 }
1121         } else {
1122                 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1123                          return ( 0, $self->loc('Permission Denied') );
1124                 }
1125         }
1126         }
1127         else {
1128     unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1129               $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1130               $self->CurrentUserHasRight('AdminGroupMembership') ) {
1131         #User has no permission to be doing this
1132         return ( 0, $self->loc("Permission Denied") );
1133     }
1134         }
1135     $self->_DeleteMember($member_id);
1136 }
1137
1138 # A helper subroutine for DeleteMember that bypasses the ACL checks
1139 # this should _ONLY_ ever be called from Ticket/Queue  DeleteWatcher
1140 # when we want to deal with groups according to queue rights
1141 # In the dim future, this will all get factored out and life
1142 # will get better       
1143
1144 sub _DeleteMember {
1145     my $self = shift;
1146     my $member_id = shift;
1147
1148     my $member_obj =  RT::GroupMember->new( $self->CurrentUser );
1149     
1150     $member_obj->LoadByCols( MemberId  => $member_id,
1151                              GroupId => $self->PrincipalId);
1152
1153
1154     #If we couldn't load it, return undef.
1155     unless ( $member_obj->Id() ) {
1156         $RT::Logger->debug("Group has no member with that id");
1157         return ( 0,$self->loc( "Group has no such member" ));
1158     }
1159
1160     #Now that we've checked ACLs and sanity, delete the groupmember
1161     my $val = $member_obj->Delete();
1162
1163     if ($val) {
1164         return ( $val, $self->loc("Member deleted") );
1165     }
1166     else {
1167         $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1168         return ( 0, $self->loc("Member not deleted" ));
1169     }
1170 }
1171
1172 # }}}
1173
1174 # {{{ sub _CleanupInvalidDelegations
1175
1176 =head2 _CleanupInvalidDelegations { InsideTransaction => undef }
1177
1178 Revokes all ACE entries delegated by members of this group which are
1179 inconsistent with their current delegation rights.  Does not perform
1180 permission checks.  Should only ever be called from inside the RT
1181 library.
1182
1183 If called from inside a transaction, specify a true value for the
1184 InsideTransaction parameter.
1185
1186 Returns a true value if the deletion succeeded; returns a false value
1187 and logs an internal error if the deletion fails (should not happen).
1188
1189 =cut
1190
1191 # XXX Currently there is a _CleanupInvalidDelegations method in both
1192 # RT::User and RT::Group.  If the recursive cleanup call for groups is
1193 # ever unrolled and merged, this code will probably want to be
1194 # factored out into RT::Principal.
1195
1196 sub _CleanupInvalidDelegations {
1197     my $self = shift;
1198     my %args = ( InsideTransaction => undef,
1199                   @_ );
1200
1201     unless ( $self->Id ) {
1202         $RT::Logger->warning("Group not loaded.");
1203         return (undef);
1204     }
1205
1206     my $in_trans = $args{InsideTransaction};
1207
1208     # TODO: Can this be unrolled such that the number of DB queries is constant rather than linear in exploded group size?
1209     my $members = $self->DeepMembersObj();
1210     $members->LimitToUsers();
1211     $RT::Handle->BeginTransaction() unless $in_trans;
1212     while ( my $member = $members->Next()) {
1213         my $ret = $member->MemberObj->_CleanupInvalidDelegations(InsideTransaction => 1,
1214                                                                  Object => $args{Object});
1215         unless ($ret) {
1216             $RT::Handle->Rollback() unless $in_trans;
1217             return (undef);
1218         }
1219     }
1220     $RT::Handle->Commit() unless $in_trans;
1221     return(1);
1222 }
1223
1224 # }}}
1225
1226 # {{{ ACL Related routines
1227
1228 # {{{ sub _Set
1229 sub _Set {
1230     my $self = shift;
1231     my %args = (
1232         Field => undef,
1233         Value => undef,
1234         TransactionType   => 'Set',
1235         RecordTransaction => 1,
1236         @_
1237     );
1238
1239         if ($self->Domain eq 'Personal') {
1240                 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1241                 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1242                         return ( 0, $self->loc('Permission Denied') );
1243                 }
1244         } else {
1245                 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1246                          return ( 0, $self->loc('Permission Denied') );
1247                 }
1248         }
1249         }
1250         else {
1251         unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1252                 return ( 0, $self->loc('Permission Denied') );
1253         }
1254         }
1255
1256     my $Old = $self->SUPER::_Value("$args{'Field'}");
1257     
1258     my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1259                                           Value => $args{'Value'} );
1260     
1261     #If we can't actually set the field to the value, don't record
1262     # a transaction. instead, get out of here.
1263     if ( $ret == 0 ) { return ( 0, $msg ); }
1264
1265     if ( $args{'RecordTransaction'} == 1 ) {
1266
1267         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1268                                                Type => $args{'TransactionType'},
1269                                                Field     => $args{'Field'},
1270                                                NewValue  => $args{'Value'},
1271                                                OldValue  => $Old,
1272                                                TimeTaken => $args{'TimeTaken'},
1273         );
1274         return ( $Trans, scalar $TransObj->Description );
1275     }
1276     else {
1277         return ( $ret, $msg );
1278     }
1279 }
1280
1281 # }}}
1282
1283
1284
1285
1286 =head2 CurrentUserHasRight RIGHTNAME
1287
1288 Returns true if the current user has the specified right for this group.
1289
1290
1291     TODO: we don't deal with membership visibility yet
1292
1293 =cut
1294
1295
1296 sub CurrentUserHasRight {
1297     my $self = shift;
1298     my $right = shift;
1299
1300
1301
1302     if ($self->Id && 
1303                 $self->CurrentUser->HasRight( Object => $self,
1304                                                                                    Right => $right )) {
1305         return(1);
1306    }
1307     elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right =>  $right )) {
1308                 return (1);
1309     } else {
1310         return(undef);
1311     }
1312
1313 }
1314
1315 # }}}
1316
1317
1318
1319
1320 # {{{ Principal related routines
1321
1322 =head2 PrincipalObj
1323
1324 Returns the principal object for this user. returns an empty RT::Principal
1325 if there's no principal object matching this user. 
1326 The response is cached. PrincipalObj should never ever change.
1327
1328 =begin testing
1329
1330 ok(my $u = RT::Group->new($RT::SystemUser));
1331 ok($u->Load(4), "Loaded the first user");
1332 ok($u->PrincipalObj->ObjectId == 4, "user 4 is the fourth principal");
1333 ok($u->PrincipalObj->PrincipalType eq 'Group' , "Principal 4 is a group");
1334
1335 =end testing
1336
1337 =cut
1338
1339
1340 sub PrincipalObj {
1341     my $self = shift;
1342     unless ($self->{'PrincipalObj'} &&
1343             ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
1344             ($self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
1345
1346             $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
1347             $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
1348                                                 'PrincipalType' => 'Group') ;
1349             }
1350     return($self->{'PrincipalObj'});
1351 }
1352
1353
1354 =head2 PrincipalId  
1355
1356 Returns this user's PrincipalId
1357
1358 =cut
1359
1360 sub PrincipalId {
1361     my $self = shift;
1362     return $self->Id;
1363 }
1364
1365 # }}}
1366
1367 sub BasicColumns {
1368     (
1369         [ Name => 'Name' ],
1370         [ Description => 'Description' ],
1371     );
1372 }
1373
1374 1;
1375
1376 =head1 AUTHOR
1377
1378 Jesse Vincent, jesse@bestpractical.com
1379
1380 =head1 SEE ALSO
1381
1382 RT
1383