2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2009 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/licenses/old-licenses/gpl-2.0.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 }}}
50 # Released under the terms of version 2 of the GNU Public License
54 RT::Group - RT\'s group object
59 my $group = new RT::Group($CurrentUser);
77 no warnings qw(redefine);
87 AdminGroup => 'Modify group metadata or delete group', # loc_pair
88 AdminGroupMembership =>
89 'Modify membership roster for this group', # loc_pair
91 "Delegate specific rights which have been granted to you.", # loc_pair
92 ModifyOwnMembership => 'Join or leave this group', # loc_pair
93 EditSavedSearches => 'Edit saved searches for this group', # loc_pair
94 ShowSavedSearches => 'Display saved searches for this group', # loc_pair
95 SeeGroup => 'Make this group visible to user', # loc_pair
97 SeeGroupDashboard => 'View dashboards for this group', #loc_pair
98 CreateGroupDashboard => 'Create dashboards for this group', #loc_pair
99 ModifyGroupDashboard => 'Modify dashboards for this group', #loc_pair
100 DeleteGroupDashboard => 'Delete dashboards for this group', #loc_pair
103 # Tell RT::ACE that this sort of object can get acls granted
104 $RT::ACE::OBJECT_TYPES{'RT::Group'} = 1;
109 # TODO: This should be refactored out into an RT::ACLedObject or something
110 # stuff the rights into a hash of rights that can exist.
112 foreach my $right ( keys %{$RIGHTS} ) {
113 $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
116 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
118 Adds the given rights to the list of possible rights. This method
119 should be called during server startup, not at runtime.
126 $RIGHTS = { %$RIGHTS, %new };
127 %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
128 map { lc($_) => $_ } keys %new);
131 =head2 AvailableRights
133 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
137 sub AvailableRights {
143 # {{{ sub SelfDescription
145 =head2 SelfDescription
147 Returns a user-readable description of what this group is for and what it's named.
151 sub SelfDescription {
153 if ($self->Domain eq 'ACLEquivalence') {
154 my $user = RT::Principal->new($self->CurrentUser);
155 $user->Load($self->Instance);
156 return $self->loc("user [_1]",$user->Object->Name);
158 elsif ($self->Domain eq 'UserDefined') {
159 return $self->loc("group '[_1]'",$self->Name);
161 elsif ($self->Domain eq 'Personal') {
162 my $user = RT::User->new($self->CurrentUser);
163 $user->Load($self->Instance);
164 return $self->loc("personal group '[_1]' for user '[_2]'",$self->Name, $user->Name);
166 elsif ($self->Domain eq 'RT::System-Role') {
167 return $self->loc("system [_1]",$self->Type);
169 elsif ($self->Domain eq 'RT::Queue-Role') {
170 my $queue = RT::Queue->new($self->CurrentUser);
171 $queue->Load($self->Instance);
172 return $self->loc("queue [_1] [_2]",$queue->Name, $self->Type);
174 elsif ($self->Domain eq 'RT::Ticket-Role') {
175 return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Type);
177 elsif ($self->Domain eq 'SystemInternal') {
178 return $self->loc("system group '[_1]'",$self->Type);
181 return $self->loc("undescribed group [_1]",$self->Id);
191 Load a group object from the database. Takes a single argument.
192 If the argument is numerical, load by the column 'id'. Otherwise,
199 my $identifier = shift || return undef;
201 if ( $identifier !~ /\D/ ) {
202 $self->SUPER::LoadById($identifier);
205 $RT::Logger->crit("Group -> Load called with a bogus argument");
212 # {{{ sub LoadUserDefinedGroup
214 =head2 LoadUserDefinedGroup NAME
216 Loads a system group from the database. The only argument is
222 sub LoadUserDefinedGroup {
224 my $identifier = shift;
226 if ( $identifier =~ /^\d+$/ ) {
227 return $self->LoadByCols(
228 Domain => 'UserDefined',
232 return $self->LoadByCols(
233 Domain => 'UserDefined',
241 # {{{ sub LoadACLEquivalenceGroup
243 =head2 LoadACLEquivalenceGroup PRINCIPAL
245 Loads a user's acl equivalence group. Takes a principal object or its ID.
246 ACL equivalnce groups are used to simplify the acl system. Each user
247 has one group that only he is a member of. Rights granted to the user
248 are actually granted to that group. This greatly simplifies ACL checks.
249 While this results in a somewhat more complex setup when creating users
250 and granting ACLs, it _greatly_ simplifies acl checks.
254 sub LoadACLEquivalenceGroup {
256 my $principal = shift;
257 $principal = $principal->id if ref $principal;
259 return $self->LoadByCols(
260 Domain => 'ACLEquivalence',
262 Instance => $principal,
268 # {{{ sub LoadPersonalGroup
270 =head2 LoadPersonalGroup {Name => NAME, User => USERID}
272 Loads a personal group from the database.
276 sub LoadPersonalGroup {
278 my %args = ( Name => undef,
282 $self->LoadByCols( "Domain" => 'Personal',
283 "Instance" => $args{'User'},
285 "Name" => $args{'Name'} );
290 # {{{ sub LoadSystemInternalGroup
292 =head2 LoadSystemInternalGroup NAME
294 Loads a Pseudo group from the database. The only argument is
300 sub LoadSystemInternalGroup {
302 my $identifier = shift;
304 return $self->LoadByCols(
305 Domain => 'SystemInternal',
312 # {{{ sub LoadTicketRoleGroup
314 =head2 LoadTicketRoleGroup { Ticket => TICKET_ID, Type => TYPE }
316 Loads a ticket group from the database.
318 Takes a param hash with 2 parameters:
320 Ticket is the TicketId we're curious about
321 Type is the type of Group we're trying to load:
322 Requestor, Cc, AdminCc, Owner
326 sub LoadTicketRoleGroup {
328 my %args = (Ticket => '0',
331 $self->LoadByCols( Domain => 'RT::Ticket-Role',
332 Instance =>$args{'Ticket'},
333 Type => $args{'Type'}
339 # {{{ sub LoadQueueRoleGroup
341 =head2 LoadQueueRoleGroup { Queue => Queue_ID, Type => TYPE }
343 Loads a Queue group from the database.
345 Takes a param hash with 2 parameters:
347 Queue is the QueueId we're curious about
348 Type is the type of Group we're trying to load:
349 Requestor, Cc, AdminCc, Owner
353 sub LoadQueueRoleGroup {
355 my %args = (Queue => undef,
358 $self->LoadByCols( Domain => 'RT::Queue-Role',
359 Instance =>$args{'Queue'},
360 Type => $args{'Type'}
366 # {{{ sub LoadSystemRoleGroup
368 =head2 LoadSystemRoleGroup Type
370 Loads a System group from the database.
372 Takes a single param: Type
374 Type is the type of Group we're trying to load:
375 Requestor, Cc, AdminCc, Owner
379 sub LoadSystemRoleGroup {
382 $self->LoadByCols( Domain => 'RT::System-Role',
393 You need to specify what sort of group you're creating by calling one of the other
394 Create_____ routines.
400 $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
401 return(0,$self->loc('Permission Denied'));
410 Takes a paramhash with named arguments: Name, Description.
412 Returns a tuple of (Id, Message). If id is 0, the create failed
420 Description => undef,
424 InsideTransaction => undef,
425 _RecordTransaction => 1,
429 $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
430 # Groups deal with principal ids, rather than user ids.
431 # When creating this group, set up a principal Id for it.
432 my $principal = RT::Principal->new( $self->CurrentUser );
433 my $principal_id = $principal->Create(
434 PrincipalType => 'Group',
437 $principal->__Set(Field => 'ObjectId', Value => $principal_id);
440 $self->SUPER::Create(
442 Name => $args{'Name'},
443 Description => $args{'Description'},
444 Type => $args{'Type'},
445 Domain => $args{'Domain'},
446 Instance => ($args{'Instance'} || '0')
450 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
451 return ( 0, $self->loc('Could not create group') );
454 # If we couldn't create a principal Id, get the fuck out.
455 unless ($principal_id) {
456 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
457 $RT::Logger->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
458 return ( 0, $self->loc('Could not create group') );
461 # Now we make the group a member of itself as a cached group member
462 # this needs to exist so that group ACL checks don't fall over.
463 # you're checking CachedGroupMembers to see if the principal in question
464 # is a member of the principal the rights have been granted too
466 # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as
467 # cached members. thankfully, we're creating the group now...so it has no members.
468 my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
469 $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
472 if ( $args{'_RecordTransaction'} ) {
473 $self->_NewTransaction( Type => "Create" );
476 $RT::Handle->Commit() unless ($args{'InsideTransaction'});
478 return ( $id, $self->loc("Group created") );
483 # {{{ CreateUserDefinedGroup
485 =head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
487 A helper subroutine which creates a system group
489 Returns a tuple of (Id, Message). If id is 0, the create failed
493 sub CreateUserDefinedGroup {
496 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
497 $RT::Logger->warning( $self->CurrentUser->Name
498 . " Tried to create a group without permission." );
499 return ( 0, $self->loc('Permission Denied') );
502 return($self->_Create( Domain => 'UserDefined', Type => '', Instance => '', @_));
507 # {{{ _CreateACLEquivalenceGroup
509 =head2 _CreateACLEquivalenceGroup { Principal }
511 A helper subroutine which creates a group containing only
512 an individual user. This gets used by the ACL system to check rights.
513 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
515 Returns a tuple of (Id, Message). If id is 0, the create failed
519 sub _CreateACLEquivalenceGroup {
523 my $id = $self->_Create( Domain => 'ACLEquivalence',
525 Name => 'User '. $princ->Object->Id,
526 Description => 'ACL equiv. for user '.$princ->Object->Id,
527 Instance => $princ->Id,
528 InsideTransaction => 1);
530 $RT::Logger->crit("Couldn't create ACL equivalence group");
534 # We use stashuser so we don't get transactions inside transactions
535 # and so we bypass all sorts of cruft we don't need
536 my $aclstash = RT::GroupMember->new($self->CurrentUser);
537 my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
541 $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
542 # We call super delete so we don't get acl checked.
543 $self->SUPER::Delete();
551 # {{{ CreatePersonalGroup
553 =head2 CreatePersonalGroup { PrincipalId => PRINCIPAL_ID, Name => "name", Description => "Description"}
555 A helper subroutine which creates a personal group. Generally,
556 personal groups are used for ACL delegation and adding to ticket roles
557 PrincipalId defaults to the current user's principal id.
559 Returns a tuple of (Id, Message). If id is 0, the create failed
563 sub CreatePersonalGroup {
567 Description => undef,
568 PrincipalId => $self->CurrentUser->PrincipalId,
572 if ( $self->CurrentUser->PrincipalId == $args{'PrincipalId'} ) {
574 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups') ) {
575 $RT::Logger->warning( $self->CurrentUser->Name
576 . " Tried to create a group without permission." );
577 return ( 0, $self->loc('Permission Denied') );
582 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
583 $RT::Logger->warning( $self->CurrentUser->Name
584 . " Tried to create a group without permission." );
585 return ( 0, $self->loc('Permission Denied') );
592 Domain => 'Personal',
594 Instance => $args{'PrincipalId'},
595 Name => $args{'Name'},
596 Description => $args{'Description'}
603 # {{{ CreateRoleGroup
605 =head2 CreateRoleGroup { Domain => DOMAIN, Type => TYPE, Instance => ID }
607 A helper subroutine which creates a ticket group. (What RT 2.0 called Ticket watchers)
608 Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner")
609 Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
610 Instance is the id of the ticket or queue in question
612 This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
614 Returns a tuple of (Id, Message). If id is 0, the create failed
618 sub CreateRoleGroup {
620 my %args = ( Instance => undef,
624 unless ( $args{'Type'} =~ /^(?:Cc|AdminCc|Requestor|Owner)$/ ) {
625 return ( 0, $self->loc("Invalid Group Type") );
629 return ( $self->_Create( Domain => $args{'Domain'},
630 Instance => $args{'Instance'},
631 Type => $args{'Type'},
632 InsideTransaction => 1 ) );
648 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
649 return ( 0, 'Permission Denied' );
652 $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
655 # Remove the principal object
656 # Remove this group from anything it's a member of.
657 # Remove all cached members of this group
658 # Remove any rights granted to this group
659 # remove any rights delegated by way of this group
661 return ( $self->SUPER::Delete(@_) );
666 =head2 SetDisabled BOOL
668 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
669 It will not appear in most group listings.
671 This routine finds all the cached group members that are members of this group (recursively) and disables them.
680 if ($self->Domain eq 'Personal') {
681 if ($self->CurrentUser->PrincipalId == $self->Instance) {
682 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
683 return ( 0, $self->loc('Permission Denied') );
686 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
687 return ( 0, $self->loc('Permission Denied') );
692 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
693 return (0, $self->loc('Permission Denied'));
696 $RT::Handle->BeginTransaction();
697 $self->PrincipalObj->SetDisabled($val);
702 # Find all occurrences of this member as a member of this group
703 # in the cache and nuke them, recursively.
705 # The following code will delete all Cached Group members
706 # where this member's group is _not_ the primary group
707 # (Ie if we're deleting C as a member of B, and B happens to be
708 # a member of A, will delete C as a member of A without touching
711 my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
713 $cached_submembers->Limit( FIELD => 'ImmediateParentId', OPERATOR => '=', VALUE => $self->Id);
715 #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space.
716 # TODO what about the groups key cache?
717 RT::Principal->InvalidateACLCache();
721 while ( my $item = $cached_submembers->Next() ) {
722 my $del_err = $item->SetDisabled($val);
724 $RT::Handle->Rollback();
725 $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
730 $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
732 $RT::Handle->Commit();
734 return (1, $self->loc("Group disabled"));
736 return (1, $self->loc("Group enabled"));
747 $self->PrincipalObj->Disabled(@_);
753 =head2 DeepMembersObj
755 Returns an RT::CachedGroupMembers object of this group's members,
756 including all members of subgroups.
762 my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
764 #If we don't have rights, don't include any results
765 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
766 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
768 return ( $members_obj );
778 Returns an RT::GroupMembers object of this group's direct members.
784 my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
786 #If we don't have rights, don't include any results
787 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
788 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
790 return ( $members_obj );
796 # {{{ GroupMembersObj
798 =head2 GroupMembersObj [Recursively => 1]
800 Returns an L<RT::Groups> object of this group's members.
801 By default returns groups including all subgroups, but
802 could be changed with C<Recursively> named argument.
804 B<Note> that groups are not filtered by type and result
805 may contain as well system groups, personal and other.
809 sub GroupMembersObj {
811 my %args = ( Recursively => 1, @_ );
813 my $groups = RT::Groups->new( $self->CurrentUser );
814 my $members_table = $args{'Recursively'}?
815 'CachedGroupMembers': 'GroupMembers';
817 my $members_alias = $groups->NewAlias( $members_table );
819 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
820 ALIAS2 => $groups->PrincipalsAlias, FIELD2 => 'id',
823 ALIAS => $members_alias,
825 VALUE => $self->PrincipalId,
828 ALIAS => $members_alias,
831 ) if $args{'Recursively'};
840 =head2 UserMembersObj
842 Returns an L<RT::Users> object of this group's members, by default
843 returns users including all members of subgroups, but could be
844 changed with C<Recursively> named argument.
850 my %args = ( Recursively => 1, @_ );
852 #If we don't have rights, don't include any results
853 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
855 my $members_table = $args{'Recursively'}?
856 'CachedGroupMembers': 'GroupMembers';
858 my $users = RT::Users->new($self->CurrentUser);
859 my $members_alias = $users->NewAlias( $members_table );
861 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
862 ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id',
865 ALIAS => $members_alias,
867 VALUE => $self->PrincipalId,
870 ALIAS => $members_alias,
873 ) if $args{'Recursively'};
880 # {{{ MemberEmailAddresses
882 =head2 MemberEmailAddresses
884 Returns an array of the email addresses of all of this group's members
889 sub MemberEmailAddresses {
893 my $members = $self->UserMembersObj();
894 while (my $member = $members->Next) {
895 $addresses{$member->EmailAddress} = 1;
897 return(sort keys %addresses);
902 # {{{ MemberEmailAddressesAsString
904 =head2 MemberEmailAddressesAsString
906 Returns a comma delimited string of the email addresses of all users
907 who are members of this group.
912 sub MemberEmailAddressesAsString {
914 return (join(', ', $self->MemberEmailAddresses));
921 =head2 AddMember PRINCIPAL_ID
923 AddMember adds a principal to this group. It takes a single principal id.
924 Returns a two value array. the first value is true on successful
925 addition or 0 on failure. The second value is a textual status msg.
931 my $new_member = shift;
935 if ($self->Domain eq 'Personal') {
936 if ($self->CurrentUser->PrincipalId == $self->Instance) {
937 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
938 return ( 0, $self->loc('Permission Denied') );
941 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
942 return ( 0, $self->loc('Permission Denied') );
948 # We should only allow membership changes if the user has the right
949 # to modify group membership or the user is the principal in question
950 # and the user has the right to modify his own membership
951 unless ( ($new_member == $self->CurrentUser->PrincipalId &&
952 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
953 $self->CurrentUserHasRight('AdminGroupMembership') ) {
954 #User has no permission to be doing this
955 return ( 0, $self->loc("Permission Denied") );
959 $self->_AddMember(PrincipalId => $new_member);
962 # A helper subroutine for AddMember that bypasses the ACL checks
963 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
964 # when we want to deal with groups according to queue rights
965 # In the dim future, this will all get factored out and life
968 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
972 my %args = ( PrincipalId => undef,
973 InsideTransaction => undef,
975 my $new_member = $args{'PrincipalId'};
978 $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
979 return(0, $self->loc("Group not found"));
982 unless ($new_member =~ /^\d+$/) {
983 $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
987 my $new_member_obj = RT::Principal->new( $self->CurrentUser );
988 $new_member_obj->Load($new_member);
991 unless ( $new_member_obj->Id ) {
992 $RT::Logger->debug("Couldn't find that principal");
993 return ( 0, $self->loc("Couldn't find that principal") );
996 if ( $self->HasMember( $new_member_obj ) ) {
998 #User is already a member of this group. no need to add it
999 return ( 0, $self->loc("Group already has member: [_1]", $new_member_obj->Object->Name) );
1001 if ( $new_member_obj->IsGroup &&
1002 $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
1004 #This group can't be made to be a member of itself
1005 return ( 0, $self->loc("Groups can't be members of their members"));
1009 my $member_object = RT::GroupMember->new( $self->CurrentUser );
1010 my $id = $member_object->Create(
1011 Member => $new_member_obj,
1012 Group => $self->PrincipalObj,
1013 InsideTransaction => $args{'InsideTransaction'}
1016 return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
1019 return(0, $self->loc("Couldn't add member to group"));
1026 =head2 HasMember RT::Principal|id
1028 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a
1029 member of this group.
1030 Returns undef if the user isn't a member of the group or if the current
1031 user doesn't have permission to find out. Arguably, it should differentiate
1032 between ACL failure and non membership.
1038 my $principal = shift;
1041 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1042 $id = $principal->id;
1043 } elsif ( $principal =~ /^\d+$/ ) {
1046 $RT::Logger->error("Group::HasMember was called with an argument that".
1047 " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
1050 return undef unless $id;
1052 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1053 $member_obj->LoadByCols(
1055 GroupId => $self->PrincipalId
1058 if ( my $member_id = $member_obj->id ) {
1068 # {{{ HasMemberRecursively
1070 =head2 HasMemberRecursively RT::Principal|id
1072 Takes an L<RT::Principal> object or its id and returns true if that user is a member of
1074 Returns undef if the user isn't a member of the group or if the current
1075 user doesn't have permission to find out. Arguably, it should differentiate
1076 between ACL failure and non membership.
1080 sub HasMemberRecursively {
1082 my $principal = shift;
1085 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1086 $id = $principal->id;
1087 } elsif ( $principal =~ /^\d+$/ ) {
1090 $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1091 " isn't an RT::Principal or id. It's $principal");
1094 return undef unless $id;
1096 my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1097 $member_obj->LoadByCols(
1099 GroupId => $self->PrincipalId
1102 if ( my $member_id = $member_obj->id ) {
1114 =head2 DeleteMember PRINCIPAL_ID
1116 Takes the principal id of a current user or group.
1117 If the current user has apropriate rights,
1118 removes that GroupMember from this group.
1119 Returns a two value array. the first value is true on successful
1120 addition or 0 on failure. The second value is a textual status msg.
1126 my $member_id = shift;
1129 # We should only allow membership changes if the user has the right
1130 # to modify group membership or the user is the principal in question
1131 # and the user has the right to modify his own membership
1133 if ($self->Domain eq 'Personal') {
1134 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1135 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1136 return ( 0, $self->loc('Permission Denied') );
1139 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1140 return ( 0, $self->loc('Permission Denied') );
1145 unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1146 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1147 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1148 #User has no permission to be doing this
1149 return ( 0, $self->loc("Permission Denied") );
1152 $self->_DeleteMember($member_id);
1155 # A helper subroutine for DeleteMember that bypasses the ACL checks
1156 # this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
1157 # when we want to deal with groups according to queue rights
1158 # In the dim future, this will all get factored out and life
1163 my $member_id = shift;
1165 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1167 $member_obj->LoadByCols( MemberId => $member_id,
1168 GroupId => $self->PrincipalId);
1171 #If we couldn't load it, return undef.
1172 unless ( $member_obj->Id() ) {
1173 $RT::Logger->debug("Group has no member with that id");
1174 return ( 0,$self->loc( "Group has no such member" ));
1177 #Now that we've checked ACLs and sanity, delete the groupmember
1178 my $val = $member_obj->Delete();
1181 return ( $val, $self->loc("Member deleted") );
1184 $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1185 return ( 0, $self->loc("Member not deleted" ));
1191 # {{{ sub _CleanupInvalidDelegations
1193 =head2 _CleanupInvalidDelegations { InsideTransaction => undef }
1195 Revokes all ACE entries delegated by members of this group which are
1196 inconsistent with their current delegation rights. Does not perform
1197 permission checks. Should only ever be called from inside the RT
1200 If called from inside a transaction, specify a true value for the
1201 InsideTransaction parameter.
1203 Returns a true value if the deletion succeeded; returns a false value
1204 and logs an internal error if the deletion fails (should not happen).
1208 # XXX Currently there is a _CleanupInvalidDelegations method in both
1209 # RT::User and RT::Group. If the recursive cleanup call for groups is
1210 # ever unrolled and merged, this code will probably want to be
1211 # factored out into RT::Principal.
1213 sub _CleanupInvalidDelegations {
1215 my %args = ( InsideTransaction => undef,
1218 unless ( $self->Id ) {
1219 $RT::Logger->warning("Group not loaded.");
1223 my $in_trans = $args{InsideTransaction};
1225 # TODO: Can this be unrolled such that the number of DB queries is constant rather than linear in exploded group size?
1226 my $members = $self->DeepMembersObj();
1227 $members->LimitToUsers();
1228 $RT::Handle->BeginTransaction() unless $in_trans;
1229 while ( my $member = $members->Next()) {
1230 my $ret = $member->MemberObj->_CleanupInvalidDelegations(InsideTransaction => 1,
1231 Object => $args{Object});
1233 $RT::Handle->Rollback() unless $in_trans;
1237 $RT::Handle->Commit() unless $in_trans;
1243 # {{{ ACL Related routines
1251 TransactionType => 'Set',
1252 RecordTransaction => 1,
1256 if ($self->Domain eq 'Personal') {
1257 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1258 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1259 return ( 0, $self->loc('Permission Denied') );
1262 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1263 return ( 0, $self->loc('Permission Denied') );
1268 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1269 return ( 0, $self->loc('Permission Denied') );
1273 my $Old = $self->SUPER::_Value("$args{'Field'}");
1275 my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1276 Value => $args{'Value'} );
1278 #If we can't actually set the field to the value, don't record
1279 # a transaction. instead, get out of here.
1280 if ( $ret == 0 ) { return ( 0, $msg ); }
1282 if ( $args{'RecordTransaction'} == 1 ) {
1284 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1285 Type => $args{'TransactionType'},
1286 Field => $args{'Field'},
1287 NewValue => $args{'Value'},
1289 TimeTaken => $args{'TimeTaken'},
1291 return ( $Trans, scalar $TransObj->Description );
1294 return ( $ret, $msg );
1303 =head2 CurrentUserHasRight RIGHTNAME
1305 Returns true if the current user has the specified right for this group.
1308 TODO: we don't deal with membership visibility yet
1313 sub CurrentUserHasRight {
1320 $self->CurrentUser->HasRight( Object => $self,
1321 Right => $right )) {
1324 elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right => $right )) {
1337 # {{{ Principal related routines
1341 Returns the principal object for this user. returns an empty RT::Principal
1342 if there's no principal object matching this user.
1343 The response is cached. PrincipalObj should never ever change.
1351 unless ( defined $self->{'PrincipalObj'} &&
1352 defined $self->{'PrincipalObj'}->ObjectId &&
1353 ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
1354 (defined $self->{'PrincipalObj'}->PrincipalType &&
1355 $self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
1357 $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
1358 $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
1359 'PrincipalType' => 'Group') ;
1361 return($self->{'PrincipalObj'});
1367 Returns this user's PrincipalId
1381 [ Description => 'Description' ],
1389 Jesse Vincent, jesse@bestpractical.com