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