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