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