2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC
7 # <jesse@bestpractical.com>
9 # (Except where explicitly superseded by other copyright notices)
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
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.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/copyleft/gpl.html.
31 # CONTRIBUTION SUBMISSION POLICY:
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
48 # END BPS TAGGED BLOCK }}}
49 # Released under the terms of version 2 of the GNU Public License
53 RT::Group - RT\'s group object
58 my $group = new RT::Group($CurrentUser);
70 ok (require RT::Group);
72 ok (my $group = RT::Group->new($RT::SystemUser), "instantiated a group object");
73 ok (my ($id, $msg) = $group->CreateUserDefinedGroup( Name => 'TestGroup', Description => 'A test group',
74 ), 'Created a new group');
75 ok ($id != 0, "Group id is $id");
76 ok ($group->Name eq 'TestGroup', "The group's name is 'TestGroup'");
77 my $ng = RT::Group->new($RT::SystemUser);
79 ok($ng->LoadUserDefinedGroup('TestGroup'), "Loaded testgroup");
80 ok(($ng->id == $group->id), "Loaded the right group");
83 ok (($id,$msg) = $ng->AddMember('1'), "Added a member to the group");
85 ok (($id,$msg) = $ng->AddMember('2' ), "Added a member to the group");
87 ok (($id,$msg) = $ng->AddMember('3' ), "Added a member to the group");
90 # Group 1 now has members 1, 2 ,3
92 my $group_2 = RT::Group->new($RT::SystemUser);
93 ok (my ($id_2, $msg_2) = $group_2->CreateUserDefinedGroup( Name => 'TestGroup2', Description => 'A second test group'), , 'Created a new group');
94 ok ($id_2 != 0, "Created group 2 ok- $msg_2 ");
95 ok (($id,$msg) = $group_2->AddMember($ng->PrincipalId), "Made TestGroup a member of testgroup2");
97 ok (($id,$msg) = $group_2->AddMember('1' ), "Added member RT_System to the group TestGroup2");
100 # Group 2 how has 1, g1->{1, 2,3}
102 my $group_3 = RT::Group->new($RT::SystemUser);
103 ok (($id_3, $msg) = $group_3->CreateUserDefinedGroup( Name => 'TestGroup3', Description => 'A second test group'), 'Created a new group');
104 ok ($id_3 != 0, "Created group 3 ok - $msg");
105 ok (($id,$msg) =$group_3->AddMember($group_2->PrincipalId), "Made TestGroup a member of testgroup2");
108 # g3 now has g2->{1, g1->{1,2,3}}
110 my $principal_1 = RT::Principal->new($RT::SystemUser);
111 $principal_1->Load('1');
113 my $principal_2 = RT::Principal->new($RT::SystemUser);
114 $principal_2->Load('2');
116 ok (($id,$msg) = $group_3->AddMember('1' ), "Added member RT_System to the group TestGroup2");
119 # g3 now has 1, g2->{1, g1->{1,2,3}}
121 ok($group_3->HasMember($principal_2) == undef, "group 3 doesn't have member 2");
122 ok($group_3->HasMemberRecursively($principal_2), "group 3 has member 2 recursively");
123 ok($ng->HasMember($principal_2) , "group ".$ng->Id." has member 2");
124 my ($delid , $delmsg) =$ng->DeleteMember($principal_2->Id);
125 ok ($delid !=0, "Sucessfully deleted it-".$delid."-".$delmsg);
127 #Gotta reload the group objects, since we've been messing with various internals.
128 # we shouldn't need to do this.
129 #$ng->LoadUserDefinedGroup('TestGroup');
130 #$group_2->LoadUserDefinedGroup('TestGroup2');
131 #$group_3->LoadUserDefinedGroup('TestGroup');
134 # Group 2 how has 1, g1->{1, 3}
135 # g3 now has 1, g2->{1, g1->{1, 3}}
137 ok(!$ng->HasMember($principal_2) , "group ".$ng->Id." no longer has member 2");
138 ok($group_3->HasMemberRecursively($principal_2) == undef, "group 3 doesn't have member 2");
139 ok($group_2->HasMemberRecursively($principal_2) == undef, "group 2 doesn't have member 2");
140 ok($ng->HasMember($principal_2) == undef, "group 1 doesn't have member 2");;
141 ok($group_3->HasMemberRecursively($principal_2) == undef, "group 3 has member 2 recursively");
155 no warnings qw(redefine);
158 use RT::GroupMembers;
162 use vars qw/$RIGHTS/;
165 AdminGroup => 'Modify group metadata or delete group', # loc_pair
166 AdminGroupMembership =>
167 'Modify membership roster for this group', # loc_pair
168 ModifyOwnMembership => 'Join or leave this group', # loc_pair
169 EditSavedSearches => 'Edit saved searches for this group', # loc_pair
170 ShowSavedSearches => 'Display saved searches for this group', # loc_pair
171 SeeGroup => 'Make this group visible to user', # loc_pair
174 # Tell RT::ACE that this sort of object can get acls granted
175 $RT::ACE::OBJECT_TYPES{'RT::Group'} = 1;
180 # TODO: This should be refactored out into an RT::ACLedObject or something
181 # stuff the rights into a hash of rights that can exist.
183 foreach my $right ( keys %{$RIGHTS} ) {
184 $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
188 =head2 AvailableRights
190 Returns a hash of available rights for this object. The keys are the right names and the values are a description of what the rights do
194 sub AvailableRights {
200 # {{{ sub SelfDescription
202 =head2 SelfDescription
204 Returns a user-readable description of what this group is for and what it's named.
208 sub SelfDescription {
210 if ($self->Domain eq 'ACLEquivalence') {
211 my $user = RT::Principal->new($self->CurrentUser);
212 $user->Load($self->Instance);
213 return $self->loc("user [_1]",$user->Object->Name);
215 elsif ($self->Domain eq 'UserDefined') {
216 return $self->loc("group '[_1]'",$self->Name);
218 elsif ($self->Domain eq 'Personal') {
219 my $user = RT::User->new($self->CurrentUser);
220 $user->Load($self->Instance);
221 return $self->loc("personal group '[_1]' for user '[_2]'",$self->Name, $user->Name);
223 elsif ($self->Domain eq 'RT::System-Role') {
224 return $self->loc("system [_1]",$self->Type);
226 elsif ($self->Domain eq 'RT::Queue-Role') {
227 my $queue = RT::Queue->new($self->CurrentUser);
228 $queue->Load($self->Instance);
229 return $self->loc("queue [_1] [_2]",$queue->Name, $self->Type);
231 elsif ($self->Domain eq 'RT::Ticket-Role') {
232 return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Type);
234 elsif ($self->Domain eq 'SystemInternal') {
235 return $self->loc("system group '[_1]'",$self->Type);
238 return $self->loc("undescribed group [_1]",$self->Id);
248 Load a group object from the database. Takes a single argument.
249 If the argument is numerical, load by the column 'id'. Otherwise,
256 my $identifier = shift || return undef;
258 #if it's an int, load by id. otherwise, load by name.
259 if ( $identifier !~ /\D/ ) {
260 $self->SUPER::LoadById($identifier);
263 $RT::Logger->crit("Group -> Load called with a bogus argument");
270 # {{{ sub LoadUserDefinedGroup
272 =head2 LoadUserDefinedGroup NAME
274 Loads a system group from the database. The only argument is
280 sub LoadUserDefinedGroup {
282 my $identifier = shift;
284 $self->LoadByCols( "Domain" => 'UserDefined',
285 "Name" => $identifier );
290 # {{{ sub LoadACLEquivalenceGroup
292 =head2 LoadACLEquivalenceGroup PRINCIPAL
294 Loads a user's acl equivalence group. Takes a principal object.
295 ACL equivalnce groups are used to simplify the acl system. Each user
296 has one group that only he is a member of. Rights granted to the user
297 are actually granted to that group. This greatly simplifies ACL checks.
298 While this results in a somewhat more complex setup when creating users
299 and granting ACLs, it _greatly_ simplifies acl checks.
305 sub LoadACLEquivalenceGroup {
309 $self->LoadByCols( "Domain" => 'ACLEquivalence',
310 "Type" => 'UserEquiv',
311 "Instance" => $princ->Id);
316 # {{{ sub LoadPersonalGroup
318 =head2 LoadPersonalGroup {Name => NAME, User => USERID}
320 Loads a personal group from the database.
324 sub LoadPersonalGroup {
326 my %args = ( Name => undef,
330 $self->LoadByCols( "Domain" => 'Personal',
331 "Instance" => $args{'User'},
333 "Name" => $args{'Name'} );
338 # {{{ sub LoadSystemInternalGroup
340 =head2 LoadSystemInternalGroup NAME
342 Loads a Pseudo group from the database. The only argument is
348 sub LoadSystemInternalGroup {
350 my $identifier = shift;
352 $self->LoadByCols( "Domain" => 'SystemInternal',
353 "Type" => $identifier );
358 # {{{ sub LoadTicketRoleGroup
360 =head2 LoadTicketRoleGroup { Ticket => TICKET_ID, Type => TYPE }
362 Loads a ticket group from the database.
364 Takes a param hash with 2 parameters:
366 Ticket is the TicketId we're curious about
367 Type is the type of Group we're trying to load:
368 Requestor, Cc, AdminCc, Owner
372 sub LoadTicketRoleGroup {
374 my %args = (Ticket => '0',
377 $self->LoadByCols( Domain => 'RT::Ticket-Role',
378 Instance =>$args{'Ticket'},
379 Type => $args{'Type'}
385 # {{{ sub LoadQueueRoleGroup
387 =head2 LoadQueueRoleGroup { Queue => Queue_ID, Type => TYPE }
389 Loads a Queue group from the database.
391 Takes a param hash with 2 parameters:
393 Queue is the QueueId we're curious about
394 Type is the type of Group we're trying to load:
395 Requestor, Cc, AdminCc, Owner
399 sub LoadQueueRoleGroup {
401 my %args = (Queue => undef,
404 $self->LoadByCols( Domain => 'RT::Queue-Role',
405 Instance =>$args{'Queue'},
406 Type => $args{'Type'}
412 # {{{ sub LoadSystemRoleGroup
414 =head2 LoadSystemRoleGroup Type
416 Loads a System group from the database.
418 Takes a single param: Type
420 Type is the type of Group we're trying to load:
421 Requestor, Cc, AdminCc, Owner
425 sub LoadSystemRoleGroup {
428 $self->LoadByCols( Domain => 'RT::System-Role',
439 You need to specify what sort of group you're creating by calling one of the other
440 Create_____ routines.
446 $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
447 return(0,$self->loc('Permission Denied'));
456 Takes a paramhash with named arguments: Name, Description.
458 Returns a tuple of (Id, Message). If id is 0, the create failed
466 Description => undef,
470 InsideTransaction => undef,
471 _RecordTransaction => 1,
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',
483 $principal->__Set(Field => 'ObjectId', Value => $principal_id);
486 $self->SUPER::Create(
488 Name => $args{'Name'},
489 Description => $args{'Description'},
490 Type => $args{'Type'},
491 Domain => $args{'Domain'},
492 Instance => ($args{'Instance'} || '0')
496 return ( 0, $self->loc('Could not create group') );
499 # If we couldn't create a principal Id, get the fuck out.
500 unless ($principal_id) {
501 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
502 $RT::Logger->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
503 return ( 0, $self->loc('Could not create group') );
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
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);
517 if ( $args{'_RecordTransaction'} ) {
518 $self->_NewTransaction( Type => "Create" );
521 $RT::Handle->Commit() unless ($args{'InsideTransaction'});
523 return ( $id, $self->loc("Group created") );
528 # {{{ CreateUserDefinedGroup
530 =head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
532 A helper subroutine which creates a system group
534 Returns a tuple of (Id, Message). If id is 0, the create failed
538 sub CreateUserDefinedGroup {
541 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
542 $RT::Logger->warning( $self->CurrentUser->Name
543 . " Tried to create a group without permission." );
544 return ( 0, $self->loc('Permission Denied') );
547 return($self->_Create( Domain => 'UserDefined', Type => '', Instance => '', @_));
552 # {{{ _CreateACLEquivalenceGroup
554 =head2 _CreateACLEquivalenceGroup { Principal }
556 A helper subroutine which creates a group containing only
557 an individual user. This gets used by the ACL system to check rights.
558 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
560 Returns a tuple of (Id, Message). If id is 0, the create failed
564 sub _CreateACLEquivalenceGroup {
568 my $id = $self->_Create( Domain => 'ACLEquivalence',
570 Name => 'User '. $princ->Object->Id,
571 Description => 'ACL equiv. for user '.$princ->Object->Id,
572 Instance => $princ->Id,
573 InsideTransaction => 1);
575 $RT::Logger->crit("Couldn't create ACL equivalence group");
579 # We use stashuser so we don't get transactions inside transactions
580 # and so we bypass all sorts of cruft we don't need
581 my $aclstash = RT::GroupMember->new($self->CurrentUser);
582 my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
586 $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
587 # We call super delete so we don't get acl checked.
588 $self->SUPER::Delete();
596 # {{{ CreatePersonalGroup
598 =head2 CreatePersonalGroup { PrincipalId => PRINCIPAL_ID, Name => "name", Description => "Description"}
600 A helper subroutine which creates a personal group. Generally,
601 personal groups are used for ACL delegation and adding to ticket roles
602 PrincipalId defaults to the current user's principal id.
604 Returns a tuple of (Id, Message). If id is 0, the create failed
608 sub CreatePersonalGroup {
612 Description => undef,
613 PrincipalId => $self->CurrentUser->PrincipalId,
617 if ( $self->CurrentUser->PrincipalId == $args{'PrincipalId'} ) {
619 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups') ) {
620 $RT::Logger->warning( $self->CurrentUser->Name
621 . " Tried to create a group without permission." );
622 return ( 0, $self->loc('Permission Denied') );
627 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
628 $RT::Logger->warning( $self->CurrentUser->Name
629 . " Tried to create a group without permission." );
630 return ( 0, $self->loc('Permission Denied') );
637 Domain => 'Personal',
639 Instance => $args{'PrincipalId'},
640 Name => $args{'Name'},
641 Description => $args{'Description'}
648 # {{{ CreateRoleGroup
650 =head2 CreateRoleGroup { Domain => DOMAIN, Type => TYPE, Instance => ID }
652 A helper subroutine which creates a ticket group. (What RT 2.0 called Ticket watchers)
653 Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner")
654 Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
655 Instance is the id of the ticket or queue in question
657 This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
659 Returns a tuple of (Id, Message). If id is 0, the create failed
663 sub CreateRoleGroup {
665 my %args = ( Instance => undef,
669 unless ( $args{'Type'} =~ /^(?:Cc|AdminCc|Requestor|Owner)$/ ) {
670 return ( 0, $self->loc("Invalid Group Type") );
674 return ( $self->_Create( Domain => $args{'Domain'},
675 Instance => $args{'Instance'},
676 Type => $args{'Type'},
677 InsideTransaction => 1 ) );
693 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
694 return ( 0, 'Permission Denied' );
697 $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
700 # Remove the principal object
701 # Remove this group from anything it's a member of.
702 # Remove all cached members of this group
703 # Remove any rights granted to this group
704 # remove any rights delegated by way of this group
706 return ( $self->SUPER::Delete(@_) );
711 =head2 SetDisabled BOOL
713 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
714 It will not appear in most group listings.
716 This routine finds all the cached group members that are members of this group (recursively) and disables them.
725 if ($self->Domain eq 'Personal') {
726 if ($self->CurrentUser->PrincipalId == $self->Instance) {
727 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
728 return ( 0, $self->loc('Permission Denied') );
731 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
732 return ( 0, $self->loc('Permission Denied') );
737 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
738 return (0, $self->loc('Permission Denied'));
741 $RT::Handle->BeginTransaction();
742 $self->PrincipalObj->SetDisabled($val);
747 # Find all occurrences of this member as a member of this group
748 # in the cache and nuke them, recursively.
750 # The following code will delete all Cached Group members
751 # where this member's group is _not_ the primary group
752 # (Ie if we're deleting C as a member of B, and B happens to be
753 # a member of A, will delete C as a member of A without touching
756 my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
758 $cached_submembers->Limit( FIELD => 'ImmediateParentId', OPERATOR => '=', VALUE => $self->Id);
760 #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space.
761 # TODO what about the groups key cache?
762 RT::Principal->InvalidateACLCache();
766 while ( my $item = $cached_submembers->Next() ) {
767 my $del_err = $item->SetDisabled($val);
769 $RT::Handle->Rollback();
770 $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
775 $RT::Handle->Commit();
776 return (1, $self->loc("Succeeded"));
786 $self->PrincipalObj->Disabled(@_);
792 =head2 DeepMembersObj
794 Returns an RT::CachedGroupMembers object of this group's members,
795 including all members of subgroups.
801 my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
803 #If we don't have rights, don't include any results
804 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
805 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
807 return ( $members_obj );
815 =head2 UserMembersObj
817 Returns an RT::Users object of this group's members, including
818 all members of subgroups
825 my $users = RT::Users->new($self->CurrentUser);
827 #If we don't have rights, don't include any results
828 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
830 my $cached_members = $users->NewAlias('CachedGroupMembers');
831 $users->Join(ALIAS1 => $cached_members, FIELD1 => 'MemberId',
832 ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id');
833 $users->Limit(ALIAS => $cached_members,
836 VALUE => $self->PrincipalId);
848 Returns an RT::GroupMembers object of this group's direct members.
854 my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
856 #If we don't have rights, don't include any results
857 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
858 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
860 return ( $members_obj );
866 # {{{ MemberEmailAddresses
868 =head2 MemberEmailAddresses
870 Returns an array of the email addresses of all of this group's members
875 sub MemberEmailAddresses {
879 my $members = $self->UserMembersObj();
880 while (my $member = $members->Next) {
881 $addresses{$member->EmailAddress} = 1;
883 return(sort keys %addresses);
888 # {{{ MemberEmailAddressesAsString
890 =head2 MemberEmailAddressesAsString
892 Returns a comma delimited string of the email addresses of all users
893 who are members of this group.
898 sub MemberEmailAddressesAsString {
900 return (join(', ', $self->MemberEmailAddresses));
907 =head2 AddMember PRINCIPAL_ID
909 AddMember adds a principal to this group. It takes a single principal id.
910 Returns a two value array. the first value is true on successful
911 addition or 0 on failure. The second value is a textual status msg.
917 my $new_member = shift;
921 if ($self->Domain eq 'Personal') {
922 if ($self->CurrentUser->PrincipalId == $self->Instance) {
923 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
924 return ( 0, $self->loc('Permission Denied') );
927 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
928 return ( 0, $self->loc('Permission Denied') );
934 # We should only allow membership changes if the user has the right
935 # to modify group membership or the user is the principal in question
936 # and the user has the right to modify his own membership
937 unless ( ($new_member == $self->CurrentUser->PrincipalId &&
938 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
939 $self->CurrentUserHasRight('AdminGroupMembership') ) {
940 #User has no permission to be doing this
941 return ( 0, $self->loc("Permission Denied") );
945 $self->_AddMember(PrincipalId => $new_member);
948 # A helper subroutine for AddMember that bypasses the ACL checks
949 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
950 # when we want to deal with groups according to queue rights
951 # In the dim future, this will all get factored out and life
954 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
958 my %args = ( PrincipalId => undef,
959 InsideTransaction => undef,
961 my $new_member = $args{'PrincipalId'};
964 $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
965 return(0, $self->loc("Group not found"));
968 unless ($new_member =~ /^\d+$/) {
969 $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
973 my $new_member_obj = RT::Principal->new( $self->CurrentUser );
974 $new_member_obj->Load($new_member);
977 unless ( $new_member_obj->Id ) {
978 $RT::Logger->debug("Couldn't find that principal");
979 return ( 0, $self->loc("Couldn't find that principal") );
982 if ( $self->HasMember( $new_member_obj ) ) {
984 #User is already a member of this group. no need to add it
985 return ( 0, $self->loc("Group already has member") );
987 if ( $new_member_obj->IsGroup &&
988 $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
990 #This group can't be made to be a member of itself
991 return ( 0, $self->loc("Groups can't be members of their members"));
995 my $member_object = RT::GroupMember->new( $self->CurrentUser );
996 my $id = $member_object->Create(
997 Member => $new_member_obj,
998 Group => $self->PrincipalObj,
999 InsideTransaction => $args{'InsideTransaction'}
1002 return ( 1, $self->loc("Member added") );
1005 return(0, $self->loc("Couldn't add member to group"));
1012 =head2 HasMember RT::Principal
1014 Takes an RT::Principal object returns a GroupMember Id if that user is a
1015 member of this group.
1016 Returns undef if the user isn't a member of the group or if the current
1017 user doesn't have permission to find out. Arguably, it should differentiate
1018 between ACL failure and non membership.
1024 my $principal = shift;
1027 unless (UNIVERSAL::isa($principal,'RT::Principal')) {
1028 $RT::Logger->crit("Group::HasMember was called with an argument that".
1029 "isn't an RT::Principal. It's $principal");
1033 unless ($principal->Id) {
1037 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1038 $member_obj->LoadByCols( MemberId => $principal->id,
1039 GroupId => $self->PrincipalId );
1041 #If we have a member object
1042 if ( defined $member_obj->id ) {
1043 return ( $member_obj->id );
1046 #If Load returns no objects, we have an undef id.
1048 #$RT::Logger->debug($self." does not contain principal ".$principal->id);
1055 # {{{ HasMemberRecursively
1057 =head2 HasMemberRecursively RT::Principal
1059 Takes an RT::Principal object and returns true if that user is a member of
1061 Returns undef if the user isn't a member of the group or if the current
1062 user doesn't have permission to find out. Arguably, it should differentiate
1063 between ACL failure and non membership.
1067 sub HasMemberRecursively {
1069 my $principal = shift;
1071 unless (UNIVERSAL::isa($principal,'RT::Principal')) {
1072 $RT::Logger->crit("Group::HasMemberRecursively was called with an argument that".
1073 "isn't an RT::Principal. It's $principal");
1076 my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1077 $member_obj->LoadByCols( MemberId => $principal->Id,
1078 GroupId => $self->PrincipalId ,
1082 #If we have a member object
1083 if ( defined $member_obj->id ) {
1087 #If Load returns no objects, we have an undef id.
1097 =head2 DeleteMember PRINCIPAL_ID
1099 Takes the principal id of a current user or group.
1100 If the current user has apropriate rights,
1101 removes that GroupMember from this group.
1102 Returns a two value array. the first value is true on successful
1103 addition or 0 on failure. The second value is a textual status msg.
1109 my $member_id = shift;
1112 # We should only allow membership changes if the user has the right
1113 # to modify group membership or the user is the principal in question
1114 # and the user has the right to modify his own membership
1116 if ($self->Domain eq 'Personal') {
1117 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1118 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1119 return ( 0, $self->loc('Permission Denied') );
1122 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1123 return ( 0, $self->loc('Permission Denied') );
1128 unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1129 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1130 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1131 #User has no permission to be doing this
1132 return ( 0, $self->loc("Permission Denied") );
1135 $self->_DeleteMember($member_id);
1138 # A helper subroutine for DeleteMember that bypasses the ACL checks
1139 # this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
1140 # when we want to deal with groups according to queue rights
1141 # In the dim future, this will all get factored out and life
1146 my $member_id = shift;
1148 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1150 $member_obj->LoadByCols( MemberId => $member_id,
1151 GroupId => $self->PrincipalId);
1154 #If we couldn't load it, return undef.
1155 unless ( $member_obj->Id() ) {
1156 $RT::Logger->debug("Group has no member with that id");
1157 return ( 0,$self->loc( "Group has no such member" ));
1160 #Now that we've checked ACLs and sanity, delete the groupmember
1161 my $val = $member_obj->Delete();
1164 return ( $val, $self->loc("Member deleted") );
1167 $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1168 return ( 0, $self->loc("Member not deleted" ));
1174 # {{{ sub _CleanupInvalidDelegations
1176 =head2 _CleanupInvalidDelegations { InsideTransaction => undef }
1178 Revokes all ACE entries delegated by members of this group which are
1179 inconsistent with their current delegation rights. Does not perform
1180 permission checks. Should only ever be called from inside the RT
1183 If called from inside a transaction, specify a true value for the
1184 InsideTransaction parameter.
1186 Returns a true value if the deletion succeeded; returns a false value
1187 and logs an internal error if the deletion fails (should not happen).
1191 # XXX Currently there is a _CleanupInvalidDelegations method in both
1192 # RT::User and RT::Group. If the recursive cleanup call for groups is
1193 # ever unrolled and merged, this code will probably want to be
1194 # factored out into RT::Principal.
1196 sub _CleanupInvalidDelegations {
1198 my %args = ( InsideTransaction => undef,
1201 unless ( $self->Id ) {
1202 $RT::Logger->warning("Group not loaded.");
1206 my $in_trans = $args{InsideTransaction};
1208 # TODO: Can this be unrolled such that the number of DB queries is constant rather than linear in exploded group size?
1209 my $members = $self->DeepMembersObj();
1210 $members->LimitToUsers();
1211 $RT::Handle->BeginTransaction() unless $in_trans;
1212 while ( my $member = $members->Next()) {
1213 my $ret = $member->MemberObj->_CleanupInvalidDelegations(InsideTransaction => 1,
1214 Object => $args{Object});
1216 $RT::Handle->Rollback() unless $in_trans;
1220 $RT::Handle->Commit() unless $in_trans;
1226 # {{{ ACL Related routines
1234 TransactionType => 'Set',
1235 RecordTransaction => 1,
1239 if ($self->Domain eq 'Personal') {
1240 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1241 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1242 return ( 0, $self->loc('Permission Denied') );
1245 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1246 return ( 0, $self->loc('Permission Denied') );
1251 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1252 return ( 0, $self->loc('Permission Denied') );
1256 my $Old = $self->SUPER::_Value("$args{'Field'}");
1258 my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1259 Value => $args{'Value'} );
1261 #If we can't actually set the field to the value, don't record
1262 # a transaction. instead, get out of here.
1263 if ( $ret == 0 ) { return ( 0, $msg ); }
1265 if ( $args{'RecordTransaction'} == 1 ) {
1267 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1268 Type => $args{'TransactionType'},
1269 Field => $args{'Field'},
1270 NewValue => $args{'Value'},
1272 TimeTaken => $args{'TimeTaken'},
1274 return ( $Trans, scalar $TransObj->Description );
1277 return ( $ret, $msg );
1286 =head2 CurrentUserHasRight RIGHTNAME
1288 Returns true if the current user has the specified right for this group.
1291 TODO: we don't deal with membership visibility yet
1296 sub CurrentUserHasRight {
1303 $self->CurrentUser->HasRight( Object => $self,
1304 Right => $right )) {
1307 elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right => $right )) {
1320 # {{{ Principal related routines
1324 Returns the principal object for this user. returns an empty RT::Principal
1325 if there's no principal object matching this user.
1326 The response is cached. PrincipalObj should never ever change.
1330 ok(my $u = RT::Group->new($RT::SystemUser));
1331 ok($u->Load(4), "Loaded the first user");
1332 ok($u->PrincipalObj->ObjectId == 4, "user 4 is the fourth principal");
1333 ok($u->PrincipalObj->PrincipalType eq 'Group' , "Principal 4 is a group");
1342 unless ($self->{'PrincipalObj'} &&
1343 ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
1344 ($self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
1346 $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
1347 $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
1348 'PrincipalType' => 'Group') ;
1350 return($self->{'PrincipalObj'});
1356 Returns this user's PrincipalId
1370 [ Description => 'Description' ],
1378 Jesse Vincent, jesse@bestpractical.com