2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
29 # CONTRIBUTION SUBMISSION POLICY:
31 # (The following paragraph is not intended to limit the rights granted
32 # to you to modify and distribute this software under the terms of
33 # the GNU General Public License and is only of importance to you if
34 # you choose to contribute your changes and enhancements to the
35 # community by submitting them to Best Practical Solutions, LLC.)
37 # By intentionally submitting any modifications, corrections or
38 # derivatives to this work, or any other work intended for use with
39 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
40 # you are the copyright holder for those contributions and you grant
41 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
42 # royalty-free, perpetual, license to use, copy, create derivative
43 # works based on those contributions, and sublicense and distribute
44 # those contributions and any derivatives thereof.
46 # END BPS TAGGED BLOCK }}}
47 # Released under the terms of version 2 of the GNU Public License
51 RT::Group - RT\'s group object
56 my $group = new RT::Group($CurrentUser);
68 ok (require RT::Group);
70 ok (my $group = RT::Group->new($RT::SystemUser), "instantiated a group object");
71 ok (my ($id, $msg) = $group->CreateUserDefinedGroup( Name => 'TestGroup', Description => 'A test group',
72 ), 'Created a new group');
73 ok ($id != 0, "Group id is $id");
74 ok ($group->Name eq 'TestGroup', "The group's name is 'TestGroup'");
75 my $ng = RT::Group->new($RT::SystemUser);
77 ok($ng->LoadUserDefinedGroup('TestGroup'), "Loaded testgroup");
78 ok(($ng->id == $group->id), "Loaded the right group");
81 ok (($id,$msg) = $ng->AddMember('1'), "Added a member to the group");
83 ok (($id,$msg) = $ng->AddMember('2' ), "Added a member to the group");
85 ok (($id,$msg) = $ng->AddMember('3' ), "Added a member to the group");
88 # Group 1 now has members 1, 2 ,3
90 my $group_2 = RT::Group->new($RT::SystemUser);
91 ok (my ($id_2, $msg_2) = $group_2->CreateUserDefinedGroup( Name => 'TestGroup2', Description => 'A second test group'), , 'Created a new group');
92 ok ($id_2 != 0, "Created group 2 ok- $msg_2 ");
93 ok (($id,$msg) = $group_2->AddMember($ng->PrincipalId), "Made TestGroup a member of testgroup2");
95 ok (($id,$msg) = $group_2->AddMember('1' ), "Added member RT_System to the group TestGroup2");
98 # Group 2 how has 1, g1->{1, 2,3}
100 my $group_3 = RT::Group->new($RT::SystemUser);
101 ok (($id_3, $msg) = $group_3->CreateUserDefinedGroup( Name => 'TestGroup3', Description => 'A second test group'), 'Created a new group');
102 ok ($id_3 != 0, "Created group 3 ok - $msg");
103 ok (($id,$msg) =$group_3->AddMember($group_2->PrincipalId), "Made TestGroup a member of testgroup2");
106 # g3 now has g2->{1, g1->{1,2,3}}
108 my $principal_1 = RT::Principal->new($RT::SystemUser);
109 $principal_1->Load('1');
111 my $principal_2 = RT::Principal->new($RT::SystemUser);
112 $principal_2->Load('2');
114 ok (($id,$msg) = $group_3->AddMember('1' ), "Added member RT_System to the group TestGroup2");
117 # g3 now has 1, g2->{1, g1->{1,2,3}}
119 ok($group_3->HasMember($principal_2) == undef, "group 3 doesn't have member 2");
120 ok($group_3->HasMemberRecursively($principal_2), "group 3 has member 2 recursively");
121 ok($ng->HasMember($principal_2) , "group ".$ng->Id." has member 2");
122 my ($delid , $delmsg) =$ng->DeleteMember($principal_2->Id);
123 ok ($delid !=0, "Sucessfully deleted it-".$delid."-".$delmsg);
125 #Gotta reload the group objects, since we've been messing with various internals.
126 # we shouldn't need to do this.
127 #$ng->LoadUserDefinedGroup('TestGroup');
128 #$group_2->LoadUserDefinedGroup('TestGroup2');
129 #$group_3->LoadUserDefinedGroup('TestGroup');
132 # Group 2 how has 1, g1->{1, 3}
133 # g3 now has 1, g2->{1, g1->{1, 3}}
135 ok(!$ng->HasMember($principal_2) , "group ".$ng->Id." no longer has member 2");
136 ok($group_3->HasMemberRecursively($principal_2) == undef, "group 3 doesn't have member 2");
137 ok($group_2->HasMemberRecursively($principal_2) == undef, "group 2 doesn't have member 2");
138 ok($ng->HasMember($principal_2) == undef, "group 1 doesn't have member 2");;
139 ok($group_3->HasMemberRecursively($principal_2) == undef, "group 3 has member 2 recursively");
153 no warnings qw(redefine);
156 use RT::GroupMembers;
160 use vars qw/$RIGHTS/;
163 AdminGroup => 'Modify group metadata or delete group', # loc_pair
164 AdminGroupMembership =>
165 'Modify membership roster for this group', # loc_pair
166 ModifyOwnMembership => 'Join or leave this group', # loc_pair
167 EditSavedSearches => 'Edit saved searches for this group', # loc_pair
168 ShowSavedSearches => 'Display saved searches for this group', # loc_pair
169 SeeGroup => 'Make this group visible to user', # loc_pair
172 # Tell RT::ACE that this sort of object can get acls granted
173 $RT::ACE::OBJECT_TYPES{'RT::Group'} = 1;
178 # TODO: This should be refactored out into an RT::ACLedObject or something
179 # stuff the rights into a hash of rights that can exist.
181 foreach my $right ( keys %{$RIGHTS} ) {
182 $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
186 =head2 AvailableRights
188 Returns a hash of available rights for this object. The keys are the right names and the values are a description of what the rights do
192 sub AvailableRights {
198 # {{{ sub SelfDescription
200 =head2 SelfDescription
202 Returns a user-readable description of what this group is for and what it's named.
206 sub SelfDescription {
208 if ($self->Domain eq 'ACLEquivalence') {
209 my $user = RT::Principal->new($self->CurrentUser);
210 $user->Load($self->Instance);
211 return $self->loc("user [_1]",$user->Object->Name);
213 elsif ($self->Domain eq 'UserDefined') {
214 return $self->loc("group '[_1]'",$self->Name);
216 elsif ($self->Domain eq 'Personal') {
217 my $user = RT::User->new($self->CurrentUser);
218 $user->Load($self->Instance);
219 return $self->loc("personal group '[_1]' for user '[_2]'",$self->Name, $user->Name);
221 elsif ($self->Domain eq 'RT::System-Role') {
222 return $self->loc("system [_1]",$self->Type);
224 elsif ($self->Domain eq 'RT::Queue-Role') {
225 my $queue = RT::Queue->new($self->CurrentUser);
226 $queue->Load($self->Instance);
227 return $self->loc("queue [_1] [_2]",$queue->Name, $self->Type);
229 elsif ($self->Domain eq 'RT::Ticket-Role') {
230 return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Type);
232 elsif ($self->Domain eq 'SystemInternal') {
233 return $self->loc("system group '[_1]'",$self->Type);
236 return $self->loc("undescribed group [_1]",$self->Id);
246 Load a group object from the database. Takes a single argument.
247 If the argument is numerical, load by the column 'id'. Otherwise,
254 my $identifier = shift || return undef;
256 #if it's an int, load by id. otherwise, load by name.
257 if ( $identifier !~ /\D/ ) {
258 $self->SUPER::LoadById($identifier);
261 $RT::Logger->crit("Group -> Load called with a bogus argument");
268 # {{{ sub LoadUserDefinedGroup
270 =head2 LoadUserDefinedGroup NAME
272 Loads a system group from the database. The only argument is
278 sub LoadUserDefinedGroup {
280 my $identifier = shift;
282 $self->LoadByCols( "Domain" => 'UserDefined',
283 "Name" => $identifier );
288 # {{{ sub LoadACLEquivalenceGroup
290 =head2 LoadACLEquivalenceGroup PRINCIPAL
292 Loads a user's acl equivalence group. Takes a principal object.
293 ACL equivalnce groups are used to simplify the acl system. Each user
294 has one group that only he is a member of. Rights granted to the user
295 are actually granted to that group. This greatly simplifies ACL checks.
296 While this results in a somewhat more complex setup when creating users
297 and granting ACLs, it _greatly_ simplifies acl checks.
303 sub LoadACLEquivalenceGroup {
307 $self->LoadByCols( "Domain" => 'ACLEquivalence',
308 "Type" => 'UserEquiv',
309 "Instance" => $princ->Id);
314 # {{{ sub LoadPersonalGroup
316 =head2 LoadPersonalGroup {Name => NAME, User => USERID}
318 Loads a personal group from the database.
322 sub LoadPersonalGroup {
324 my %args = ( Name => undef,
328 $self->LoadByCols( "Domain" => 'Personal',
329 "Instance" => $args{'User'},
331 "Name" => $args{'Name'} );
336 # {{{ sub LoadSystemInternalGroup
338 =head2 LoadSystemInternalGroup NAME
340 Loads a Pseudo group from the database. The only argument is
346 sub LoadSystemInternalGroup {
348 my $identifier = shift;
350 $self->LoadByCols( "Domain" => 'SystemInternal',
351 "Type" => $identifier );
356 # {{{ sub LoadTicketRoleGroup
358 =head2 LoadTicketRoleGroup { Ticket => TICKET_ID, Type => TYPE }
360 Loads a ticket group from the database.
362 Takes a param hash with 2 parameters:
364 Ticket is the TicketId we're curious about
365 Type is the type of Group we're trying to load:
366 Requestor, Cc, AdminCc, Owner
370 sub LoadTicketRoleGroup {
372 my %args = (Ticket => '0',
375 $self->LoadByCols( Domain => 'RT::Ticket-Role',
376 Instance =>$args{'Ticket'},
377 Type => $args{'Type'}
383 # {{{ sub LoadQueueRoleGroup
385 =head2 LoadQueueRoleGroup { Queue => Queue_ID, Type => TYPE }
387 Loads a Queue group from the database.
389 Takes a param hash with 2 parameters:
391 Queue is the QueueId we're curious about
392 Type is the type of Group we're trying to load:
393 Requestor, Cc, AdminCc, Owner
397 sub LoadQueueRoleGroup {
399 my %args = (Queue => undef,
402 $self->LoadByCols( Domain => 'RT::Queue-Role',
403 Instance =>$args{'Queue'},
404 Type => $args{'Type'}
410 # {{{ sub LoadSystemRoleGroup
412 =head2 LoadSystemRoleGroup Type
414 Loads a System group from the database.
416 Takes a single param: Type
418 Type is the type of Group we're trying to load:
419 Requestor, Cc, AdminCc, Owner
423 sub LoadSystemRoleGroup {
426 $self->LoadByCols( Domain => 'RT::System-Role',
437 You need to specify what sort of group you're creating by calling one of the other
438 Create_____ routines.
444 $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
445 return(0,$self->loc('Permission Denied'));
454 Takes a paramhash with named arguments: Name, Description.
456 Returns a tuple of (Id, Message). If id is 0, the create failed
464 Description => undef,
468 InsideTransaction => undef,
469 _RecordTransaction => 1,
473 $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
474 # Groups deal with principal ids, rather than user ids.
475 # When creating this group, set up a principal Id for it.
476 my $principal = RT::Principal->new( $self->CurrentUser );
477 my $principal_id = $principal->Create(
478 PrincipalType => 'Group',
481 $principal->__Set(Field => 'ObjectId', Value => $principal_id);
484 $self->SUPER::Create(
486 Name => $args{'Name'},
487 Description => $args{'Description'},
488 Type => $args{'Type'},
489 Domain => $args{'Domain'},
490 Instance => ($args{'Instance'} || '0')
494 return ( 0, $self->loc('Could not create group') );
497 # If we couldn't create a principal Id, get the fuck out.
498 unless ($principal_id) {
499 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
500 $self->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
501 return ( 0, $self->loc('Could not create group') );
504 # Now we make the group a member of itself as a cached group member
505 # this needs to exist so that group ACL checks don't fall over.
506 # you're checking CachedGroupMembers to see if the principal in question
507 # is a member of the principal the rights have been granted too
509 # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as
510 # cached members. thankfully, we're creating the group now...so it has no members.
511 my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
512 $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
515 if ( $args{'_RecordTransaction'} ) {
516 $self->_NewTransaction( Type => "Create" );
519 $RT::Handle->Commit() unless ($args{'InsideTransaction'});
521 return ( $id, $self->loc("Group created") );
526 # {{{ CreateUserDefinedGroup
528 =head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
530 A helper subroutine which creates a system group
532 Returns a tuple of (Id, Message). If id is 0, the create failed
536 sub CreateUserDefinedGroup {
539 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
540 $RT::Logger->warning( $self->CurrentUser->Name
541 . " Tried to create a group without permission." );
542 return ( 0, $self->loc('Permission Denied') );
545 return($self->_Create( Domain => 'UserDefined', Type => '', Instance => '', @_));
550 # {{{ _CreateACLEquivalenceGroup
552 =head2 _CreateACLEquivalenceGroup { Principal }
554 A helper subroutine which creates a group containing only
555 an individual user. This gets used by the ACL system to check rights.
556 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
558 Returns a tuple of (Id, Message). If id is 0, the create failed
562 sub _CreateACLEquivalenceGroup {
566 my $id = $self->_Create( Domain => 'ACLEquivalence',
568 Name => 'User '. $princ->Object->Id,
569 Description => 'ACL equiv. for user '.$princ->Object->Id,
570 Instance => $princ->Id,
571 InsideTransaction => 1);
573 $RT::Logger->crit("Couldn't create ACL equivalence group");
577 # We use stashuser so we don't get transactions inside transactions
578 # and so we bypass all sorts of cruft we don't need
579 my $aclstash = RT::GroupMember->new($self->CurrentUser);
580 my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
584 $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
585 # We call super delete so we don't get acl checked.
586 $self->SUPER::Delete();
594 # {{{ CreatePersonalGroup
596 =head2 CreatePersonalGroup { PrincipalId => PRINCIPAL_ID, Name => "name", Description => "Description"}
598 A helper subroutine which creates a personal group. Generally,
599 personal groups are used for ACL delegation and adding to ticket roles
600 PrincipalId defaults to the current user's principal id.
602 Returns a tuple of (Id, Message). If id is 0, the create failed
606 sub CreatePersonalGroup {
610 Description => undef,
611 PrincipalId => $self->CurrentUser->PrincipalId,
615 if ( $self->CurrentUser->PrincipalId == $args{'PrincipalId'} ) {
617 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups') ) {
618 $RT::Logger->warning( $self->CurrentUser->Name
619 . " Tried to create a group without permission." );
620 return ( 0, $self->loc('Permission Denied') );
625 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
626 $RT::Logger->warning( $self->CurrentUser->Name
627 . " Tried to create a group without permission." );
628 return ( 0, $self->loc('Permission Denied') );
635 Domain => 'Personal',
637 Instance => $args{'PrincipalId'},
638 Name => $args{'Name'},
639 Description => $args{'Description'}
646 # {{{ CreateRoleGroup
648 =head2 CreateRoleGroup { Domain => DOMAIN, Type => TYPE, Instance => ID }
650 A helper subroutine which creates a ticket group. (What RT 2.0 called Ticket watchers)
651 Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner")
652 Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
653 Instance is the id of the ticket or queue in question
655 This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
657 Returns a tuple of (Id, Message). If id is 0, the create failed
661 sub CreateRoleGroup {
663 my %args = ( Instance => undef,
667 unless ( $args{'Type'} =~ /^(?:Cc|AdminCc|Requestor|Owner)$/ ) {
668 return ( 0, $self->loc("Invalid Group Type") );
672 return ( $self->_Create( Domain => $args{'Domain'},
673 Instance => $args{'Instance'},
674 Type => $args{'Type'},
675 InsideTransaction => 1 ) );
691 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
692 return ( 0, 'Permission Denied' );
695 $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
698 # Remove the principal object
699 # Remove this group from anything it's a member of.
700 # Remove all cached members of this group
701 # Remove any rights granted to this group
702 # remove any rights delegated by way of this group
704 return ( $self->SUPER::Delete(@_) );
709 =head2 SetDisabled BOOL
711 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
712 It will not appear in most group listings.
714 This routine finds all the cached group members that are members of this group (recursively) and disables them.
723 if ($self->Domain eq 'Personal') {
724 if ($self->CurrentUser->PrincipalId == $self->Instance) {
725 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
726 return ( 0, $self->loc('Permission Denied') );
729 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
730 return ( 0, $self->loc('Permission Denied') );
735 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
736 return (0, $self->loc('Permission Denied'));
739 $RT::Handle->BeginTransaction();
740 $self->PrincipalObj->SetDisabled($val);
745 # Find all occurrences of this member as a member of this group
746 # in the cache and nuke them, recursively.
748 # The following code will delete all Cached Group members
749 # where this member's group is _not_ the primary group
750 # (Ie if we're deleting C as a member of B, and B happens to be
751 # a member of A, will delete C as a member of A without touching
754 my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
756 $cached_submembers->Limit( FIELD => 'ImmediateParentId', OPERATOR => '=', VALUE => $self->Id);
758 #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space.
759 # TODO what about the groups key cache?
760 RT::Principal->InvalidateACLCache();
764 while ( my $item = $cached_submembers->Next() ) {
765 my $del_err = $item->SetDisabled($val);
767 $RT::Handle->Rollback();
768 $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
773 $RT::Handle->Commit();
774 return (1, $self->loc("Succeeded"));
784 $self->PrincipalObj->Disabled(@_);
790 =head2 DeepMembersObj
792 Returns an RT::CachedGroupMembers object of this group's members,
793 including all members of subgroups.
799 my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
801 #If we don't have rights, don't include any results
802 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
803 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
805 return ( $members_obj );
813 =head2 UserMembersObj
815 Returns an RT::Users object of this group's members, including
816 all members of subgroups
823 my $users = RT::Users->new($self->CurrentUser);
825 #If we don't have rights, don't include any results
826 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
828 my $cached_members = $users->NewAlias('CachedGroupMembers');
829 $users->Join(ALIAS1 => $cached_members, FIELD1 => 'MemberId',
830 ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id');
831 $users->Limit(ALIAS => $cached_members,
834 VALUE => $self->PrincipalId);
846 Returns an RT::GroupMembers object of this group's direct members.
852 my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
854 #If we don't have rights, don't include any results
855 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
856 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
858 return ( $members_obj );
864 # {{{ MemberEmailAddresses
866 =head2 MemberEmailAddresses
868 Returns an array of the email addresses of all of this group's members
873 sub MemberEmailAddresses {
877 my $members = $self->UserMembersObj();
878 while (my $member = $members->Next) {
879 $addresses{$member->EmailAddress} = 1;
881 return(sort keys %addresses);
886 # {{{ MemberEmailAddressesAsString
888 =head2 MemberEmailAddressesAsString
890 Returns a comma delimited string of the email addresses of all users
891 who are members of this group.
896 sub MemberEmailAddressesAsString {
898 return (join(', ', $self->MemberEmailAddresses));
905 =head2 AddMember PRINCIPAL_ID
907 AddMember adds a principal to this group. It takes a single principal id.
908 Returns a two value array. the first value is true on successful
909 addition or 0 on failure. The second value is a textual status msg.
915 my $new_member = shift;
919 if ($self->Domain eq 'Personal') {
920 if ($self->CurrentUser->PrincipalId == $self->Instance) {
921 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
922 return ( 0, $self->loc('Permission Denied') );
925 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
926 return ( 0, $self->loc('Permission Denied') );
932 # We should only allow membership changes if the user has the right
933 # to modify group membership or the user is the principal in question
934 # and the user has the right to modify his own membership
935 unless ( ($new_member == $self->CurrentUser->PrincipalId &&
936 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
937 $self->CurrentUserHasRight('AdminGroupMembership') ) {
938 #User has no permission to be doing this
939 return ( 0, $self->loc("Permission Denied") );
943 $self->_AddMember(PrincipalId => $new_member);
946 # A helper subroutine for AddMember that bypasses the ACL checks
947 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
948 # when we want to deal with groups according to queue rights
949 # In the dim future, this will all get factored out and life
952 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
956 my %args = ( PrincipalId => undef,
957 InsideTransaction => undef,
959 my $new_member = $args{'PrincipalId'};
962 $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
963 return(0, $self->loc("Group not found"));
966 unless ($new_member =~ /^\d+$/) {
967 $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
971 my $new_member_obj = RT::Principal->new( $self->CurrentUser );
972 $new_member_obj->Load($new_member);
975 unless ( $new_member_obj->Id ) {
976 $RT::Logger->debug("Couldn't find that principal");
977 return ( 0, $self->loc("Couldn't find that principal") );
980 if ( $self->HasMember( $new_member_obj ) ) {
982 #User is already a member of this group. no need to add it
983 return ( 0, $self->loc("Group already has member") );
985 if ( $new_member_obj->IsGroup &&
986 $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
988 #This group can't be made to be a member of itself
989 return ( 0, $self->loc("Groups can't be members of their members"));
993 my $member_object = RT::GroupMember->new( $self->CurrentUser );
994 my $id = $member_object->Create(
995 Member => $new_member_obj,
996 Group => $self->PrincipalObj,
997 InsideTransaction => $args{'InsideTransaction'}
1000 return ( 1, $self->loc("Member added") );
1003 return(0, $self->loc("Couldn't add member to group"));
1010 =head2 HasMember RT::Principal
1012 Takes an RT::Principal object returns a GroupMember Id if that user is a
1013 member of this group.
1014 Returns undef if the user isn't a member of the group or if the current
1015 user doesn't have permission to find out. Arguably, it should differentiate
1016 between ACL failure and non membership.
1022 my $principal = shift;
1025 unless (UNIVERSAL::isa($principal,'RT::Principal')) {
1026 $RT::Logger->crit("Group::HasMember was called with an argument that".
1027 "isn't an RT::Principal. It's $principal");
1031 unless ($principal->Id) {
1035 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1036 $member_obj->LoadByCols( MemberId => $principal->id,
1037 GroupId => $self->PrincipalId );
1039 #If we have a member object
1040 if ( defined $member_obj->id ) {
1041 return ( $member_obj->id );
1044 #If Load returns no objects, we have an undef id.
1046 #$RT::Logger->debug($self." does not contain principal ".$principal->id);
1053 # {{{ HasMemberRecursively
1055 =head2 HasMemberRecursively RT::Principal
1057 Takes an RT::Principal object and returns true if that user is a member of
1059 Returns undef if the user isn't a member of the group or if the current
1060 user doesn't have permission to find out. Arguably, it should differentiate
1061 between ACL failure and non membership.
1065 sub HasMemberRecursively {
1067 my $principal = shift;
1069 unless (UNIVERSAL::isa($principal,'RT::Principal')) {
1070 $RT::Logger->crit("Group::HasMemberRecursively was called with an argument that".
1071 "isn't an RT::Principal. It's $principal");
1074 my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1075 $member_obj->LoadByCols( MemberId => $principal->Id,
1076 GroupId => $self->PrincipalId ,
1080 #If we have a member object
1081 if ( defined $member_obj->id ) {
1085 #If Load returns no objects, we have an undef id.
1095 =head2 DeleteMember PRINCIPAL_ID
1097 Takes the principal id of a current user or group.
1098 If the current user has apropriate rights,
1099 removes that GroupMember from this group.
1100 Returns a two value array. the first value is true on successful
1101 addition or 0 on failure. The second value is a textual status msg.
1107 my $member_id = shift;
1110 # We should only allow membership changes if the user has the right
1111 # to modify group membership or the user is the principal in question
1112 # and the user has the right to modify his own membership
1114 if ($self->Domain eq 'Personal') {
1115 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1116 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1117 return ( 0, $self->loc('Permission Denied') );
1120 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1121 return ( 0, $self->loc('Permission Denied') );
1126 unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1127 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1128 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1129 #User has no permission to be doing this
1130 return ( 0, $self->loc("Permission Denied") );
1133 $self->_DeleteMember($member_id);
1136 # A helper subroutine for DeleteMember that bypasses the ACL checks
1137 # this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
1138 # when we want to deal with groups according to queue rights
1139 # In the dim future, this will all get factored out and life
1144 my $member_id = shift;
1146 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1148 $member_obj->LoadByCols( MemberId => $member_id,
1149 GroupId => $self->PrincipalId);
1152 #If we couldn't load it, return undef.
1153 unless ( $member_obj->Id() ) {
1154 $RT::Logger->debug("Group has no member with that id");
1155 return ( 0,$self->loc( "Group has no such member" ));
1158 #Now that we've checked ACLs and sanity, delete the groupmember
1159 my $val = $member_obj->Delete();
1162 return ( $val, $self->loc("Member deleted") );
1165 $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1166 return ( 0, $self->loc("Member not deleted" ));
1172 # {{{ sub _CleanupInvalidDelegations
1174 =head2 _CleanupInvalidDelegations { InsideTransaction => undef }
1176 Revokes all ACE entries delegated by members of this group which are
1177 inconsistent with their current delegation rights. Does not perform
1178 permission checks. Should only ever be called from inside the RT
1181 If called from inside a transaction, specify a true value for the
1182 InsideTransaction parameter.
1184 Returns a true value if the deletion succeeded; returns a false value
1185 and logs an internal error if the deletion fails (should not happen).
1189 # XXX Currently there is a _CleanupInvalidDelegations method in both
1190 # RT::User and RT::Group. If the recursive cleanup call for groups is
1191 # ever unrolled and merged, this code will probably want to be
1192 # factored out into RT::Principal.
1194 sub _CleanupInvalidDelegations {
1196 my %args = ( InsideTransaction => undef,
1199 unless ( $self->Id ) {
1200 $RT::Logger->warning("Group not loaded.");
1204 my $in_trans = $args{InsideTransaction};
1206 # TODO: Can this be unrolled such that the number of DB queries is constant rather than linear in exploded group size?
1207 my $members = $self->DeepMembersObj();
1208 $members->LimitToUsers();
1209 $RT::Handle->BeginTransaction() unless $in_trans;
1210 while ( my $member = $members->Next()) {
1211 my $ret = $member->MemberObj->_CleanupInvalidDelegations(InsideTransaction => 1,
1212 Object => $args{Object});
1214 $RT::Handle->Rollback() unless $in_trans;
1218 $RT::Handle->Commit() unless $in_trans;
1224 # {{{ ACL Related routines
1232 TransactionType => 'Set',
1233 RecordTransaction => 1,
1237 if ($self->Domain eq 'Personal') {
1238 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1239 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1240 return ( 0, $self->loc('Permission Denied') );
1243 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1244 return ( 0, $self->loc('Permission Denied') );
1249 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1250 return ( 0, $self->loc('Permission Denied') );
1254 my $Old = $self->SUPER::_Value("$args{'Field'}");
1256 my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1257 Value => $args{'Value'} );
1259 #If we can't actually set the field to the value, don't record
1260 # a transaction. instead, get out of here.
1261 if ( $ret == 0 ) { return ( 0, $msg ); }
1263 if ( $args{'RecordTransaction'} == 1 ) {
1265 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1266 Type => $args{'TransactionType'},
1267 Field => $args{'Field'},
1268 NewValue => $args{'Value'},
1270 TimeTaken => $args{'TimeTaken'},
1272 return ( $Trans, scalar $TransObj->Description );
1275 return ( $ret, $msg );
1284 =head2 CurrentUserHasRight RIGHTNAME
1286 Returns true if the current user has the specified right for this group.
1289 TODO: we don't deal with membership visibility yet
1294 sub CurrentUserHasRight {
1301 $self->CurrentUser->HasRight( Object => $self,
1302 Right => $right )) {
1305 elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right => $right )) {
1318 # {{{ Principal related routines
1322 Returns the principal object for this user. returns an empty RT::Principal
1323 if there's no principal object matching this user.
1324 The response is cached. PrincipalObj should never ever change.
1328 ok(my $u = RT::Group->new($RT::SystemUser));
1329 ok($u->Load(4), "Loaded the first user");
1330 ok($u->PrincipalObj->ObjectId == 4, "user 4 is the fourth principal");
1331 ok($u->PrincipalObj->PrincipalType eq 'Group' , "Principal 4 is a group");
1340 unless ($self->{'PrincipalObj'} &&
1341 ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
1342 ($self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
1344 $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
1345 $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
1346 'PrincipalType' => 'Group') ;
1348 return($self->{'PrincipalObj'});
1354 Returns this user's PrincipalId
1368 [ Description => 'Description' ],
1376 Jesse Vincent, jesse@bestpractical.com