2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
7 # <sales@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,
529 _RecordTransaction => 0 );
531 $RT::Logger->crit("Couldn't create ACL equivalence group");
535 # We use stashuser so we don't get transactions inside transactions
536 # and so we bypass all sorts of cruft we don't need
537 my $aclstash = RT::GroupMember->new($self->CurrentUser);
538 my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
542 $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
543 # We call super delete so we don't get acl checked.
544 $self->SUPER::Delete();
552 # {{{ CreatePersonalGroup
554 =head2 CreatePersonalGroup { PrincipalId => PRINCIPAL_ID, Name => "name", Description => "Description"}
556 A helper subroutine which creates a personal group. Generally,
557 personal groups are used for ACL delegation and adding to ticket roles
558 PrincipalId defaults to the current user's principal id.
560 Returns a tuple of (Id, Message). If id is 0, the create failed
564 sub CreatePersonalGroup {
568 Description => undef,
569 PrincipalId => $self->CurrentUser->PrincipalId,
573 if ( $self->CurrentUser->PrincipalId == $args{'PrincipalId'} ) {
575 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups') ) {
576 $RT::Logger->warning( $self->CurrentUser->Name
577 . " Tried to create a group without permission." );
578 return ( 0, $self->loc('Permission Denied') );
583 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
584 $RT::Logger->warning( $self->CurrentUser->Name
585 . " Tried to create a group without permission." );
586 return ( 0, $self->loc('Permission Denied') );
593 Domain => 'Personal',
595 Instance => $args{'PrincipalId'},
596 Name => $args{'Name'},
597 Description => $args{'Description'}
604 # {{{ CreateRoleGroup
606 =head2 CreateRoleGroup { Domain => DOMAIN, Type => TYPE, Instance => ID }
608 A helper subroutine which creates a ticket group. (What RT 2.0 called Ticket watchers)
609 Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner")
610 Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
611 Instance is the id of the ticket or queue in question
613 This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
615 Returns a tuple of (Id, Message). If id is 0, the create failed
619 sub CreateRoleGroup {
621 my %args = ( Instance => undef,
625 unless ( $args{'Type'} =~ /^(?:Cc|AdminCc|Requestor|Owner)$/ ) {
626 return ( 0, $self->loc("Invalid Group Type") );
630 return ( $self->_Create( Domain => $args{'Domain'},
631 Instance => $args{'Instance'},
632 Type => $args{'Type'},
633 InsideTransaction => 1 ) );
649 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
650 return ( 0, 'Permission Denied' );
653 $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
656 # Remove the principal object
657 # Remove this group from anything it's a member of.
658 # Remove all cached members of this group
659 # Remove any rights granted to this group
660 # remove any rights delegated by way of this group
662 return ( $self->SUPER::Delete(@_) );
667 =head2 SetDisabled BOOL
669 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
670 It will not appear in most group listings.
672 This routine finds all the cached group members that are members of this group (recursively) and disables them.
681 if ($self->Domain eq 'Personal') {
682 if ($self->CurrentUser->PrincipalId == $self->Instance) {
683 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
684 return ( 0, $self->loc('Permission Denied') );
687 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
688 return ( 0, $self->loc('Permission Denied') );
693 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
694 return (0, $self->loc('Permission Denied'));
697 $RT::Handle->BeginTransaction();
698 $self->PrincipalObj->SetDisabled($val);
703 # Find all occurrences of this member as a member of this group
704 # in the cache and nuke them, recursively.
706 # The following code will delete all Cached Group members
707 # where this member's group is _not_ the primary group
708 # (Ie if we're deleting C as a member of B, and B happens to be
709 # a member of A, will delete C as a member of A without touching
712 my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
714 $cached_submembers->Limit( FIELD => 'ImmediateParentId', OPERATOR => '=', VALUE => $self->Id);
716 #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space.
717 # TODO what about the groups key cache?
718 RT::Principal->InvalidateACLCache();
722 while ( my $item = $cached_submembers->Next() ) {
723 my $del_err = $item->SetDisabled($val);
725 $RT::Handle->Rollback();
726 $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
731 $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
733 $RT::Handle->Commit();
735 return (1, $self->loc("Group disabled"));
737 return (1, $self->loc("Group enabled"));
748 $self->PrincipalObj->Disabled(@_);
754 =head2 DeepMembersObj
756 Returns an RT::CachedGroupMembers object of this group's members,
757 including all members of subgroups.
763 my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
765 #If we don't have rights, don't include any results
766 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
767 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
769 return ( $members_obj );
779 Returns an RT::GroupMembers object of this group's direct members.
785 my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
787 #If we don't have rights, don't include any results
788 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
789 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
791 return ( $members_obj );
797 # {{{ GroupMembersObj
799 =head2 GroupMembersObj [Recursively => 1]
801 Returns an L<RT::Groups> object of this group's members.
802 By default returns groups including all subgroups, but
803 could be changed with C<Recursively> named argument.
805 B<Note> that groups are not filtered by type and result
806 may contain as well system groups, personal and other.
810 sub GroupMembersObj {
812 my %args = ( Recursively => 1, @_ );
814 my $groups = RT::Groups->new( $self->CurrentUser );
815 my $members_table = $args{'Recursively'}?
816 'CachedGroupMembers': 'GroupMembers';
818 my $members_alias = $groups->NewAlias( $members_table );
820 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
821 ALIAS2 => $groups->PrincipalsAlias, FIELD2 => 'id',
824 ALIAS => $members_alias,
826 VALUE => $self->PrincipalId,
829 ALIAS => $members_alias,
832 ) if $args{'Recursively'};
841 =head2 UserMembersObj
843 Returns an L<RT::Users> object of this group's members, by default
844 returns users including all members of subgroups, but could be
845 changed with C<Recursively> named argument.
851 my %args = ( Recursively => 1, @_ );
853 #If we don't have rights, don't include any results
854 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
856 my $members_table = $args{'Recursively'}?
857 'CachedGroupMembers': 'GroupMembers';
859 my $users = RT::Users->new($self->CurrentUser);
860 my $members_alias = $users->NewAlias( $members_table );
862 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
863 ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id',
866 ALIAS => $members_alias,
868 VALUE => $self->PrincipalId,
871 ALIAS => $members_alias,
874 ) if $args{'Recursively'};
881 # {{{ MemberEmailAddresses
883 =head2 MemberEmailAddresses
885 Returns an array of the email addresses of all of this group's members
890 sub MemberEmailAddresses {
894 my $members = $self->UserMembersObj();
895 while (my $member = $members->Next) {
896 $addresses{$member->EmailAddress} = 1;
898 return(sort keys %addresses);
903 # {{{ MemberEmailAddressesAsString
905 =head2 MemberEmailAddressesAsString
907 Returns a comma delimited string of the email addresses of all users
908 who are members of this group.
913 sub MemberEmailAddressesAsString {
915 return (join(', ', $self->MemberEmailAddresses));
922 =head2 AddMember PRINCIPAL_ID
924 AddMember adds a principal to this group. It takes a single principal id.
925 Returns a two value array. the first value is true on successful
926 addition or 0 on failure. The second value is a textual status msg.
932 my $new_member = shift;
936 if ($self->Domain eq 'Personal') {
937 if ($self->CurrentUser->PrincipalId == $self->Instance) {
938 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
939 return ( 0, $self->loc('Permission Denied') );
942 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
943 return ( 0, $self->loc('Permission Denied') );
949 # We should only allow membership changes if the user has the right
950 # to modify group membership or the user is the principal in question
951 # and the user has the right to modify his own membership
952 unless ( ($new_member == $self->CurrentUser->PrincipalId &&
953 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
954 $self->CurrentUserHasRight('AdminGroupMembership') ) {
955 #User has no permission to be doing this
956 return ( 0, $self->loc("Permission Denied") );
960 $self->_AddMember(PrincipalId => $new_member);
963 # A helper subroutine for AddMember that bypasses the ACL checks
964 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
965 # when we want to deal with groups according to queue rights
966 # In the dim future, this will all get factored out and life
969 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
973 my %args = ( PrincipalId => undef,
974 InsideTransaction => undef,
976 my $new_member = $args{'PrincipalId'};
979 $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
980 return(0, $self->loc("Group not found"));
983 unless ($new_member =~ /^\d+$/) {
984 $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
988 my $new_member_obj = RT::Principal->new( $self->CurrentUser );
989 $new_member_obj->Load($new_member);
992 unless ( $new_member_obj->Id ) {
993 $RT::Logger->debug("Couldn't find that principal");
994 return ( 0, $self->loc("Couldn't find that principal") );
997 if ( $self->HasMember( $new_member_obj ) ) {
999 #User is already a member of this group. no need to add it
1000 return ( 0, $self->loc("Group already has member: [_1]", $new_member_obj->Object->Name) );
1002 if ( $new_member_obj->IsGroup &&
1003 $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
1005 #This group can't be made to be a member of itself
1006 return ( 0, $self->loc("Groups can't be members of their members"));
1010 my $member_object = RT::GroupMember->new( $self->CurrentUser );
1011 my $id = $member_object->Create(
1012 Member => $new_member_obj,
1013 Group => $self->PrincipalObj,
1014 InsideTransaction => $args{'InsideTransaction'}
1017 return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
1020 return(0, $self->loc("Couldn't add member to group"));
1027 =head2 HasMember RT::Principal|id
1029 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a
1030 member of this group.
1031 Returns undef if the user isn't a member of the group or if the current
1032 user doesn't have permission to find out. Arguably, it should differentiate
1033 between ACL failure and non membership.
1039 my $principal = shift;
1042 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1043 $id = $principal->id;
1044 } elsif ( $principal =~ /^\d+$/ ) {
1047 $RT::Logger->error("Group::HasMember was called with an argument that".
1048 " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
1051 return undef unless $id;
1053 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1054 $member_obj->LoadByCols(
1056 GroupId => $self->PrincipalId
1059 if ( my $member_id = $member_obj->id ) {
1069 # {{{ HasMemberRecursively
1071 =head2 HasMemberRecursively RT::Principal|id
1073 Takes an L<RT::Principal> object or its id and returns true if that user is a member of
1075 Returns undef if the user isn't a member of the group or if the current
1076 user doesn't have permission to find out. Arguably, it should differentiate
1077 between ACL failure and non membership.
1081 sub HasMemberRecursively {
1083 my $principal = shift;
1086 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1087 $id = $principal->id;
1088 } elsif ( $principal =~ /^\d+$/ ) {
1091 $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1092 " isn't an RT::Principal or id. It's $principal");
1095 return undef unless $id;
1097 my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1098 $member_obj->LoadByCols(
1100 GroupId => $self->PrincipalId
1103 if ( my $member_id = $member_obj->id ) {
1115 =head2 DeleteMember PRINCIPAL_ID
1117 Takes the principal id of a current user or group.
1118 If the current user has apropriate rights,
1119 removes that GroupMember from this group.
1120 Returns a two value array. the first value is true on successful
1121 addition or 0 on failure. The second value is a textual status msg.
1127 my $member_id = shift;
1130 # We should only allow membership changes if the user has the right
1131 # to modify group membership or the user is the principal in question
1132 # and the user has the right to modify his own membership
1134 if ($self->Domain eq 'Personal') {
1135 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1136 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1137 return ( 0, $self->loc('Permission Denied') );
1140 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1141 return ( 0, $self->loc('Permission Denied') );
1146 unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1147 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1148 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1149 #User has no permission to be doing this
1150 return ( 0, $self->loc("Permission Denied") );
1153 $self->_DeleteMember($member_id);
1156 # A helper subroutine for DeleteMember that bypasses the ACL checks
1157 # this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
1158 # when we want to deal with groups according to queue rights
1159 # In the dim future, this will all get factored out and life
1164 my $member_id = shift;
1166 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1168 $member_obj->LoadByCols( MemberId => $member_id,
1169 GroupId => $self->PrincipalId);
1172 #If we couldn't load it, return undef.
1173 unless ( $member_obj->Id() ) {
1174 $RT::Logger->debug("Group has no member with that id");
1175 return ( 0,$self->loc( "Group has no such member" ));
1178 #Now that we've checked ACLs and sanity, delete the groupmember
1179 my $val = $member_obj->Delete();
1182 return ( $val, $self->loc("Member deleted") );
1185 $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1186 return ( 0, $self->loc("Member not deleted" ));
1192 # {{{ sub CleanupInvalidDelegations
1194 =head2 CleanupInvalidDelegations { InsideTransaction => undef }
1196 Revokes all ACE entries delegated by members of this group which are
1197 inconsistent with their current delegation rights. Does not perform
1198 permission checks. Should only ever be called from inside the RT
1201 If called from inside a transaction, specify a true value for the
1202 InsideTransaction parameter.
1204 Returns a true value if the deletion succeeded; returns a false value
1205 and logs an internal error if the deletion fails (should not happen).
1209 # XXX Currently there is a CleanupInvalidDelegations method in both
1210 # RT::User and RT::Group. If the recursive cleanup call for groups is
1211 # ever unrolled and merged, this code will probably want to be
1212 # factored out into RT::Principal.
1214 # backcompat for 3.8.8 and before
1215 *_CleanupInvalidDelegations = \&CleanupInvalidDelegations;
1217 sub CleanupInvalidDelegations {
1219 my %args = ( InsideTransaction => undef,
1222 unless ( $self->Id ) {
1223 $RT::Logger->warning("Group not loaded.");
1227 my $in_trans = $args{InsideTransaction};
1229 # TODO: Can this be unrolled such that the number of DB queries is constant rather than linear in exploded group size?
1230 my $members = $self->DeepMembersObj();
1231 $members->LimitToUsers();
1232 $RT::Handle->BeginTransaction() unless $in_trans;
1233 while ( my $member = $members->Next()) {
1234 my $ret = $member->MemberObj->CleanupInvalidDelegations(InsideTransaction => 1,
1235 Object => $args{Object});
1237 $RT::Handle->Rollback() unless $in_trans;
1241 $RT::Handle->Commit() unless $in_trans;
1247 # {{{ ACL Related routines
1255 TransactionType => 'Set',
1256 RecordTransaction => 1,
1260 if ($self->Domain eq 'Personal') {
1261 if ($self->CurrentUser->PrincipalId == $self->Instance) {
1262 unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
1263 return ( 0, $self->loc('Permission Denied') );
1266 unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
1267 return ( 0, $self->loc('Permission Denied') );
1272 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1273 return ( 0, $self->loc('Permission Denied') );
1277 my $Old = $self->SUPER::_Value("$args{'Field'}");
1279 my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1280 Value => $args{'Value'} );
1282 #If we can't actually set the field to the value, don't record
1283 # a transaction. instead, get out of here.
1284 if ( $ret == 0 ) { return ( 0, $msg ); }
1286 if ( $args{'RecordTransaction'} == 1 ) {
1288 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1289 Type => $args{'TransactionType'},
1290 Field => $args{'Field'},
1291 NewValue => $args{'Value'},
1293 TimeTaken => $args{'TimeTaken'},
1295 return ( $Trans, scalar $TransObj->Description );
1298 return ( $ret, $msg );
1307 =head2 CurrentUserHasRight RIGHTNAME
1309 Returns true if the current user has the specified right for this group.
1312 TODO: we don't deal with membership visibility yet
1317 sub CurrentUserHasRight {
1324 $self->CurrentUser->HasRight( Object => $self,
1325 Right => $right )) {
1328 elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right => $right )) {
1338 =head2 CurrentUserCanSee
1340 Always returns 1; unfortunately, for historical reasons, users have
1341 always been able to examine groups they have indirect access to, even if
1342 they do not have SeeGroup explicitly.
1346 sub CurrentUserCanSee {
1351 # {{{ Principal related routines
1355 Returns the principal object for this user. returns an empty RT::Principal
1356 if there's no principal object matching this user.
1357 The response is cached. PrincipalObj should never ever change.
1365 unless ( defined $self->{'PrincipalObj'} &&
1366 defined $self->{'PrincipalObj'}->ObjectId &&
1367 ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
1368 (defined $self->{'PrincipalObj'}->PrincipalType &&
1369 $self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
1371 $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
1372 $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
1373 'PrincipalType' => 'Group') ;
1375 return($self->{'PrincipalObj'});
1381 Returns this user's PrincipalId
1395 [ Description => 'Description' ],
1403 Jesse Vincent, jesse@bestpractical.com