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