2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2018 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 = RT::Group->new($CurrentUser);
74 use base 'RT::Record';
76 use Role::Basic 'with';
77 with "RT::Record::Role::Rights";
88 __PACKAGE__->AddRight( Admin => AdminGroup => 'Modify group metadata or delete group'); # loc
89 __PACKAGE__->AddRight( Admin => AdminGroupMembership => 'Modify group membership roster'); # loc
90 __PACKAGE__->AddRight( Staff => ModifyOwnMembership => 'Join or leave group'); # loc
91 __PACKAGE__->AddRight( Admin => EditSavedSearches => 'Create, modify and delete saved searches'); # loc
92 __PACKAGE__->AddRight( Staff => ShowSavedSearches => 'View saved searches'); # loc
93 __PACKAGE__->AddRight( Staff => SeeGroup => 'View group'); # loc
94 __PACKAGE__->AddRight( Staff => SeeGroupDashboard => 'View group dashboards'); # loc
95 __PACKAGE__->AddRight( Admin => CreateGroupDashboard => 'Create group dashboards'); # loc
96 __PACKAGE__->AddRight( Admin => ModifyGroupDashboard => 'Modify group dashboards'); # loc
97 __PACKAGE__->AddRight( Admin => DeleteGroupDashboard => 'Delete group dashboards'); # loc
101 =head2 SelfDescription
103 Returns a user-readable description of what this group is for and what it's named.
107 sub SelfDescription {
109 if ($self->Domain eq 'ACLEquivalence') {
110 my $user = RT::Principal->new($self->CurrentUser);
111 $user->Load($self->Instance);
112 return $self->loc("user [_1]",$user->Object->Name);
114 elsif ($self->Domain eq 'UserDefined') {
115 return $self->loc("group '[_1]'",$self->Name);
117 elsif ($self->Domain eq 'RT::System-Role') {
118 return $self->loc("system [_1]",$self->Name);
120 elsif ($self->Domain eq 'RT::Queue-Role') {
121 my $queue = RT::Queue->new($self->CurrentUser);
122 $queue->Load($self->Instance);
123 return $self->loc("queue [_1] [_2]",$queue->Name, $self->Name);
125 elsif ($self->Domain eq 'RT::Ticket-Role') {
126 return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Name);
128 elsif ($self->RoleClass) {
129 my $class = lc $self->RoleClass;
130 $class =~ s/^RT:://i;
131 return $self->loc("[_1] #[_2] [_3]", $self->loc($class), $self->Instance, $self->Name);
133 elsif ($self->Domain eq 'SystemInternal') {
134 return $self->loc("system group '[_1]'",$self->Name);
137 return $self->loc("undescribed group [_1]",$self->Id);
145 Load a group object from the database. Takes a single argument.
146 If the argument is numerical, load by the column 'id'. Otherwise,
153 my $identifier = shift || return undef;
155 if ( $identifier !~ /\D/ ) {
156 $self->SUPER::LoadById($identifier);
159 $RT::Logger->crit("Group -> Load called with a bogus argument");
166 =head2 LoadUserDefinedGroup NAME
168 Loads a system group from the database. The only argument is
174 sub LoadUserDefinedGroup {
176 my $identifier = shift;
178 if ( $identifier =~ /^\d+$/ ) {
179 return $self->LoadByCols(
180 Domain => 'UserDefined',
184 return $self->LoadByCols(
185 Domain => 'UserDefined',
193 =head2 LoadACLEquivalenceGroup PRINCIPAL
195 Loads a user's acl equivalence group. Takes a principal object or its ID.
196 ACL equivalnce groups are used to simplify the acl system. Each user
197 has one group that only he is a member of. Rights granted to the user
198 are actually granted to that group. This greatly simplifies ACL checks.
199 While this results in a somewhat more complex setup when creating users
200 and granting ACLs, it _greatly_ simplifies acl checks.
204 sub LoadACLEquivalenceGroup {
206 my $principal = shift;
207 $principal = $principal->id if ref $principal;
209 return $self->LoadByCols(
210 Domain => 'ACLEquivalence',
212 Instance => $principal,
219 =head2 LoadSystemInternalGroup NAME
221 Loads a Pseudo group from the database. The only argument is
227 sub LoadSystemInternalGroup {
229 my $identifier = shift;
231 return $self->LoadByCols(
232 Domain => 'SystemInternal',
239 Takes a paramhash of Object and Name and attempts to load the suitable role
240 group for said object.
252 my $object = delete $args{Object};
254 return wantarray ? (0, $self->loc("Object passed is not loaded")) : 0
257 # Translate Object to Domain + Instance
258 $args{Domain} = ref($object) . "-Role";
259 $args{Instance} = $object->id;
261 return $self->LoadByCols(%args);
265 =head2 LoadTicketRoleGroup { Ticket => TICKET_ID, Name => TYPE }
267 Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>.
271 sub LoadTicketRoleGroup {
279 Instead => "RT::Group->LoadRoleGroup or RT::Ticket->RoleGroup",
282 $args{'Name'} = $args{'Type'} if exists $args{'Type'};
284 Domain => 'RT::Ticket-Role',
285 Instance => $args{'Ticket'},
286 Name => $args{'Name'},
292 =head2 LoadQueueRoleGroup { Queue => Queue_ID, Type => TYPE }
294 Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>.
298 sub LoadQueueRoleGroup {
306 Instead => "RT::Group->LoadRoleGroup or RT::Queue->RoleGroup",
309 $args{'Name'} = $args{'Type'} if exists $args{'Type'};
311 Domain => 'RT::Queue-Role',
312 Instance => $args{'Queue'},
313 Name => $args{'Name'},
319 =head2 LoadSystemRoleGroup Name
321 Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>.
325 sub LoadSystemRoleGroup {
329 Instead => "RT::Group->LoadRoleGroup or RT::System->RoleGroup",
333 Domain => 'RT::System-Role',
334 Instance => RT::System->Id,
342 if ( exists $args{'Type'} ) {
343 RT->Deprecated( Instead => 'Name', Arguments => 'Type', Remove => '4.4' );
344 $args{'Name'} = $args{'Type'};
346 return $self->SUPER::LoadByCols( %args );
353 You need to specify what sort of group you're creating by calling one of the other
354 Create_____ routines.
360 $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
361 return(0,$self->loc('Permission Denied'));
368 Takes a paramhash with named arguments: Name, Description.
370 Returns a tuple of (Id, Message). If id is 0, the create failed
378 Description => undef,
381 InsideTransaction => undef,
382 _RecordTransaction => 1,
385 if ( $args{'Type'} ) {
386 RT->Deprecated( Instead => 'Name', Arguments => 'Type', Remove => '4.4' );
387 $args{'Name'} = $args{'Type'};
389 $args{'Type'} = $args{'Name'};
392 # Enforce uniqueness on user defined group names
393 if ($args{'Domain'} and $args{'Domain'} eq 'UserDefined') {
394 my ($ok, $msg) = $self->_ValidateUserDefinedName($args{'Name'});
395 return ($ok, $msg) if not $ok;
398 $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
399 # Groups deal with principal ids, rather than user ids.
400 # When creating this group, set up a principal Id for it.
401 my $principal = RT::Principal->new( $self->CurrentUser );
402 my $principal_id = $principal->Create(
403 PrincipalType => 'Group',
406 $principal->__Set(Field => 'ObjectId', Value => $principal_id);
408 $self->SUPER::Create(
410 Name => $args{'Name'},
411 Description => $args{'Description'},
412 Type => $args{'Type'},
413 Domain => $args{'Domain'},
414 Instance => ($args{'Instance'} || '0')
418 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
419 return ( 0, $self->loc('Could not create group') );
422 # If we couldn't create a principal Id, get the fuck out.
423 unless ($principal_id) {
424 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
425 $RT::Logger->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
426 return ( 0, $self->loc('Could not create group') );
429 # Now we make the group a member of itself as a cached group member
430 # this needs to exist so that group ACL checks don't fall over.
431 # you're checking CachedGroupMembers to see if the principal in question
432 # is a member of the principal the rights have been granted too
434 # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as
435 # cached members. thankfully, we're creating the group now...so it has no members.
436 my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
437 $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
440 if ( $args{'_RecordTransaction'} ) {
441 $self->_NewTransaction( Type => "Create" );
444 $RT::Handle->Commit() unless ($args{'InsideTransaction'});
446 return ( $id, $self->loc("Group created") );
451 =head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
453 A helper subroutine which creates a system group
455 Returns a tuple of (Id, Message). If id is 0, the create failed
459 sub CreateUserDefinedGroup {
462 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
463 $RT::Logger->warning( $self->CurrentUser->Name
464 . " Tried to create a group without permission." );
465 return ( 0, $self->loc('Permission Denied') );
468 return($self->_Create( Domain => 'UserDefined', Instance => '', @_));
471 =head2 ValidateName VALUE
473 Enforces unique user defined group names when updating
478 my ($self, $value) = @_;
480 if ($self->Domain and $self->Domain eq 'UserDefined') {
481 my ($ok, $msg) = $self->_ValidateUserDefinedName($value);
482 # It's really too bad we can't pass along the actual error
485 return $self->SUPER::ValidateName($value);
488 =head2 _ValidateUserDefinedName VALUE
490 Returns true if the user defined group name isn't in use, false otherwise.
494 sub _ValidateUserDefinedName {
495 my ($self, $value) = @_;
497 return (0, 'Name is required') unless length $value;
499 my $dupcheck = RT::Group->new(RT->SystemUser);
500 $dupcheck->LoadUserDefinedGroup($value);
501 if ( $dupcheck->id && ( !$self->id || $self->id != $dupcheck->id ) ) {
502 return ( 0, $self->loc( "Group name '[_1]' is already in use", $value ) );
507 =head2 _CreateACLEquivalenceGroup { Principal }
509 A helper subroutine which creates a group containing only
510 an individual user. This gets used by the ACL system to check rights.
511 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
513 Returns a tuple of (Id, Message). If id is 0, the create failed
517 sub _CreateACLEquivalenceGroup {
521 my $id = $self->_Create( Domain => 'ACLEquivalence',
523 Description => 'ACL equiv. for user '.$princ->Object->Id,
524 Instance => $princ->Id,
525 InsideTransaction => 1,
526 _RecordTransaction => 0 );
528 $RT::Logger->crit("Couldn't create ACL equivalence group");
532 # We use stashuser so we don't get transactions inside transactions
533 # and so we bypass all sorts of cruft we don't need
534 my $aclstash = RT::GroupMember->new($self->CurrentUser);
535 my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
539 $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
540 # We call super delete so we don't get acl checked.
541 $self->SUPER::Delete();
550 =head2 CreateRoleGroup
552 A convenience method for creating a role group on an object.
554 This method expects to be called from B<inside of a database transaction>! If
555 you're calling it outside of one, you B<MUST> pass a false value for
558 Takes a paramhash of:
564 Required. RT's core role types are C<Requestor>, C<Cc>, C<AdminCc>, and
565 C<Owner>. Extensions may add their own.
569 Optional. The object on which this role applies, used to set Domain and
570 Instance automatically.
574 Optional. The class on which this role applies, with C<-Role> appended. RT's
575 supported core role group domains are C<RT::Ticket-Role>, C<RT::Queue-Role>,
576 and C<RT::System-Role>.
578 Not required if you pass an Object.
582 Optional. The numeric ID of the object (of the class encoded in Domain) on
583 which this role applies. If Domain is C<RT::System-Role>, Instance should be C<1>.
585 Not required if you pass an Object.
587 =item InsideTransaction
589 Optional. Defaults to true in expectation of usual call sites. If you call
590 this method while not inside a transaction, you C<MUST> pass a false value for
595 You must pass either an Object or both Domain and Instance.
597 Returns a tuple of (id, Message). If id is false, the create failed and
598 Message should contain an error string.
602 sub CreateRoleGroup {
604 my %args = ( Instance => undef,
608 InsideTransaction => 1,
611 # Translate Object to Domain + Instance
612 my $object = delete $args{Object};
614 $args{Domain} = ref($object) . "-Role";
615 $args{Instance} = $object->id;
618 unless ($args{Instance}) {
619 return ( 0, $self->loc("An Instance must be provided") );
622 unless ($self->ValidateRoleGroup(%args)) {
623 return ( 0, $self->loc("Invalid Group Name and Domain") );
626 if ( exists $args{'Type'} ) {
627 RT->Deprecated( Instead => 'Name', Arguments => 'Type', Remove => '4.4' );
628 $args{'Name'} = $args{'Type'};
631 my %create = map { $_ => $args{$_} } qw(Domain Instance Name);
633 my $duplicate = RT::Group->new( RT->SystemUser );
634 $duplicate->LoadByCols( %create );
635 if ($duplicate->id) {
636 return ( 0, $self->loc("Role group exists already") );
639 my ($id, $msg) = $self->_Create(
640 InsideTransaction => $args{InsideTransaction},
644 if ($self->SingleMemberRoleGroup) {
646 PrincipalId => RT->Nobody->Id,
647 InsideTransaction => $args{InsideTransaction},
648 RecordTransaction => 0,
658 my $domain = shift || $self->Domain;
659 return unless $domain =~ /^(.+)-Role$/;
660 return unless $1->DOES("RT::Record::Role::Roles");
664 =head2 ValidateRoleGroup
666 Takes a param hash containing Domain and Type which are expected to be values
667 passed into L</CreateRoleGroup>. Returns true if the specified Type is a
668 registered role on the specified Domain. Otherwise returns false.
672 sub ValidateRoleGroup {
675 return 0 unless $args{Domain} and ($args{Type} or $args{'Name'});
677 my $class = $self->RoleClass($args{Domain});
678 return 0 unless $class;
680 return $class->HasRole($args{Type}||$args{'Name'});
683 =head2 SingleMemberRoleGroup
687 sub SingleMemberRoleGroup {
689 my $class = $self->RoleClass;
690 return unless $class;
691 return $class->Role($self->Name)->{Single};
694 sub SingleMemberRoleGroupColumn {
696 my ($class) = $self->Domain =~ /^(.+)-Role$/;
697 return unless $class;
699 my $role = $class->Role($self->Name);
700 return unless $role->{Class} eq $class;
701 return $role->{Column};
704 sub RoleGroupObject {
706 my ($class) = $self->Domain =~ /^(.+)-Role$/;
707 return unless $class;
708 my $obj = $class->new( $self->CurrentUser );
709 $obj->Load( $self->Instance );
715 RT->Deprecated( Instead => 'Name', Remove => '4.4' );
716 return $self->_Value('Type', @_);
721 RT->Deprecated( Instead => 'Name', Remove => '4.4' );
722 return $self->SetName(@_);
729 my ($status, $msg) = $self->_Set( Field => 'Name', Value => $value );
730 return ($status, $msg) unless $status;
733 my ($status, $msg) = $self->__Set( Field => 'Type', Value => $value );
734 RT->Logger->error("Couldn't set Type: $msg") unless $status;
737 return ($status, $msg);
749 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
750 return ( 0, 'Permission Denied' );
753 $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
756 # Remove the principal object
757 # Remove this group from anything it's a member of.
758 # Remove all cached members of this group
759 # Remove any rights granted to this group
760 # remove any rights delegated by way of this group
762 return ( $self->SUPER::Delete(@_) );
766 =head2 SetDisabled BOOL
768 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
769 It will not appear in most group listings.
771 This routine finds all the cached group members that are members of this group (recursively) and disables them.
780 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
781 return (0, $self->loc('Permission Denied'));
783 $RT::Handle->BeginTransaction();
784 $self->PrincipalObj->SetDisabled($val);
789 # Find all occurrences of this member as a member of this group
790 # in the cache and nuke them, recursively.
792 # The following code will delete all Cached Group members
793 # where this member's group is _not_ the primary group
794 # (Ie if we're deleting C as a member of B, and B happens to be
795 # a member of A, will delete C as a member of A without touching
798 my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
800 $cached_submembers->Limit( FIELD => 'ImmediateParentId', OPERATOR => '=', VALUE => $self->Id);
802 #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space.
803 # TODO what about the groups key cache?
804 RT::Principal->InvalidateACLCache();
808 while ( my $item = $cached_submembers->Next() ) {
809 my $del_err = $item->SetDisabled($val);
811 $RT::Handle->Rollback();
812 $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
817 $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
819 $RT::Handle->Commit();
821 return (1, $self->loc("Group disabled"));
823 return (1, $self->loc("Group enabled"));
833 $self->PrincipalObj->Disabled(@_);
838 =head2 DeepMembersObj
840 Returns an RT::CachedGroupMembers object of this group's members,
841 including all members of subgroups.
847 my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
849 #If we don't have rights, don't include any results
850 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
851 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
853 return ( $members_obj );
861 Returns an RT::GroupMembers object of this group's direct members.
867 my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
869 #If we don't have rights, don't include any results
870 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
871 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
873 return ( $members_obj );
879 =head2 GroupMembersObj [Recursively => 1]
881 Returns an L<RT::Groups> object of this group's members.
882 By default returns groups including all subgroups, but
883 could be changed with C<Recursively> named argument.
885 B<Note> that groups are not filtered by type and result
886 may contain as well system groups and others.
890 sub GroupMembersObj {
892 my %args = ( Recursively => 1, @_ );
894 my $groups = RT::Groups->new( $self->CurrentUser );
895 my $members_table = $args{'Recursively'}?
896 'CachedGroupMembers': 'GroupMembers';
898 my $members_alias = $groups->NewAlias( $members_table );
900 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
901 ALIAS2 => $groups->PrincipalsAlias, FIELD2 => 'id',
904 ALIAS => $members_alias,
906 VALUE => $self->PrincipalId,
909 ALIAS => $members_alias,
912 ) if $args{'Recursively'};
919 =head2 UserMembersObj
921 Returns an L<RT::Users> object of this group's members, by default
922 returns users including all members of subgroups, but could be
923 changed with C<Recursively> named argument.
929 my %args = ( Recursively => 1, @_ );
931 #If we don't have rights, don't include any results
932 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
934 my $members_table = $args{'Recursively'}?
935 'CachedGroupMembers': 'GroupMembers';
937 my $users = RT::Users->new($self->CurrentUser);
938 my $members_alias = $users->NewAlias( $members_table );
940 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
941 ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id',
944 ALIAS => $members_alias,
946 VALUE => $self->PrincipalId,
949 ALIAS => $members_alias,
952 ) if $args{'Recursively'};
959 =head2 MemberEmailAddresses
961 Returns an array of the email addresses of all of this group's members
966 sub MemberEmailAddresses {
968 return sort grep defined && length,
969 map $_->EmailAddress,
970 @{ $self->UserMembersObj->ItemsArrayRef };
975 =head2 MemberEmailAddressesAsString
977 Returns a comma delimited string of the email addresses of all users
978 who are members of this group.
983 sub MemberEmailAddressesAsString {
985 return (join(', ', $self->MemberEmailAddresses));
990 =head2 AddMember PRINCIPAL_ID
992 AddMember adds a principal to this group. It takes a single principal id.
993 Returns a two value array. the first value is true on successful
994 addition or 0 on failure. The second value is a textual status msg.
1000 my $new_member = shift;
1004 # We should only allow membership changes if the user has the right
1005 # to modify group membership or the user is the principal in question
1006 # and the user has the right to modify his own membership
1007 unless ( ($new_member == $self->CurrentUser->PrincipalId &&
1008 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1009 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1010 #User has no permission to be doing this
1011 return ( 0, $self->loc("Permission Denied") );
1014 $self->_AddMember(PrincipalId => $new_member);
1017 # A helper subroutine for AddMember that bypasses the ACL checks
1018 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
1019 # when we want to deal with groups according to queue rights
1020 # In the dim future, this will all get factored out and life
1023 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
1027 my %args = ( PrincipalId => undef,
1028 InsideTransaction => undef,
1029 RecordTransaction => 1,
1032 # RecordSetTransaction is used by _DeleteMember to get one txn but not the other
1033 $args{RecordSetTransaction} = $args{RecordTransaction}
1034 unless exists $args{RecordSetTransaction};
1036 my $new_member = $args{'PrincipalId'};
1038 unless ($self->Id) {
1039 $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
1040 return(0, $self->loc("Group not found"));
1043 unless ($new_member =~ /^\d+$/) {
1044 $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
1048 my $new_member_obj = RT::Principal->new( $self->CurrentUser );
1049 $new_member_obj->Load($new_member);
1052 unless ( $new_member_obj->Id ) {
1053 $RT::Logger->debug("Couldn't find that principal");
1054 return ( 0, $self->loc("Couldn't find that principal") );
1057 if ( $self->HasMember( $new_member_obj ) ) {
1059 #User is already a member of this group. no need to add it
1060 return ( 0, $self->loc("Group already has member: [_1]", $new_member_obj->Object->Name) );
1062 if ( $new_member_obj->IsGroup &&
1063 $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
1065 #This group can't be made to be a member of itself
1066 return ( 0, $self->loc("Groups can't be members of their members"));
1070 push @purge, @{$self->MembersObj->ItemsArrayRef}
1071 if $self->SingleMemberRoleGroup;
1073 my $member_object = RT::GroupMember->new( $self->CurrentUser );
1074 my $id = $member_object->Create(
1075 Member => $new_member_obj,
1076 Group => $self->PrincipalObj,
1077 InsideTransaction => $args{'InsideTransaction'}
1080 return(0, $self->loc("Couldn't add member to group"))
1083 # Purge all previous members (we're a single member role group)
1085 for my $member (@purge) {
1086 my $old_member = $member->MemberId;
1087 my ($ok, $msg) = $member->Delete();
1088 return(0, $self->loc("Couldn't remove previous member: [_1]", $msg))
1091 # We remove all members in this loop, but there should only ever be one
1092 # member. Keep track of the last one successfully removed for the
1093 # SetWatcher transaction below.
1094 $old_member_id = $old_member;
1098 if (my $col = $self->SingleMemberRoleGroupColumn) {
1099 my $obj = $args{Object} || $self->RoleGroupObject;
1100 my ($ok, $msg) = $obj->_Set(
1102 Value => $new_member_obj->Id,
1103 CheckACL => 0, # don't check acl
1104 RecordTransaction => $args{'RecordSetTransaction'},
1106 return (0, $self->loc("Could not update column [_1]: [_2]", $col, $msg))
1110 # Record transactions for UserDefined groups
1111 if ($args{RecordTransaction} && $self->Domain eq 'UserDefined') {
1112 $new_member_obj->Object->_NewTransaction(
1113 Type => 'AddMembership',
1114 Field => $self->PrincipalObj->id,
1117 $self->_NewTransaction(
1118 Type => 'AddMember',
1119 Field => $new_member,
1123 # Record an Add/SetWatcher txn on the object if we're a role group
1124 if ($args{RecordTransaction} and $self->RoleClass) {
1125 my $obj = $args{Object} || $self->RoleGroupObject;
1127 if ($self->SingleMemberRoleGroup) {
1128 $obj->_NewTransaction(
1129 Type => 'SetWatcher',
1130 OldValue => $old_member_id,
1131 NewValue => $new_member_obj->Id,
1132 Field => $self->Name,
1135 $obj->_NewTransaction(
1136 Type => 'AddWatcher', # use "watcher" for history's sake
1137 NewValue => $new_member_obj->Id,
1138 Field => $self->Name,
1143 return (1, $self->loc("[_1] set to [_2]",
1144 $self->loc($self->Name), $new_member_obj->Object->Name) )
1145 if $self->SingleMemberRoleGroup;
1147 return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
1151 =head2 HasMember RT::Principal|id
1153 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a
1154 member of this group.
1155 Returns undef if the user isn't a member of the group or if the current
1156 user doesn't have permission to find out. Arguably, it should differentiate
1157 between ACL failure and non membership.
1163 my $principal = shift;
1166 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1167 $id = $principal->id;
1168 } elsif ( $principal =~ /^\d+$/ ) {
1171 $RT::Logger->error("Group::HasMember was called with an argument that".
1172 " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
1175 return undef unless $id;
1177 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1178 $member_obj->LoadByCols(
1180 GroupId => $self->PrincipalId
1183 if ( my $member_id = $member_obj->id ) {
1193 =head2 HasMemberRecursively RT::Principal|id
1195 Takes an L<RT::Principal> object or its id and returns true if that user is a member of
1197 Returns undef if the user isn't a member of the group or if the current
1198 user doesn't have permission to find out. Arguably, it should differentiate
1199 between ACL failure and non membership.
1203 sub HasMemberRecursively {
1205 my $principal = shift;
1208 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1209 $id = $principal->id;
1210 } elsif ( $principal =~ /^\d+$/ ) {
1213 $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1214 " isn't an RT::Principal or id. It's $principal");
1217 return undef unless $id;
1219 my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1220 $member_obj->LoadByCols(
1222 GroupId => $self->PrincipalId
1225 if ( my $member_id = $member_obj->id ) {
1235 =head2 DeleteMember PRINCIPAL_ID
1237 Takes the principal id of a current user or group.
1238 If the current user has apropriate rights,
1239 removes that GroupMember from this group.
1240 Returns a two value array. the first value is true on successful
1241 addition or 0 on failure. The second value is a textual status msg.
1243 Optionally takes a hash of key value flags, such as RecordTransaction.
1249 my $member_id = shift;
1252 # We should only allow membership changes if the user has the right
1253 # to modify group membership or the user is the principal in question
1254 # and the user has the right to modify his own membership
1256 unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1257 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1258 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1259 #User has no permission to be doing this
1260 return ( 0, $self->loc("Permission Denied") );
1262 $self->_DeleteMember($member_id, @_);
1265 # A helper subroutine for DeleteMember that bypasses the ACL checks
1266 # this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
1267 # when we want to deal with groups according to queue rights
1268 # In the dim future, this will all get factored out and life
1273 my $member_id = shift;
1275 RecordTransaction => 1,
1280 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1282 $member_obj->LoadByCols( MemberId => $member_id,
1283 GroupId => $self->PrincipalId);
1286 #If we couldn't load it, return undef.
1287 unless ( $member_obj->Id() ) {
1288 $RT::Logger->debug("Group has no member with that id");
1289 return ( 0,$self->loc( "Group has no such member" ));
1292 my $old_member = $member_obj->MemberId;
1294 #Now that we've checked ACLs and sanity, delete the groupmember
1295 my $val = $member_obj->Delete();
1298 $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1299 return ( 0, $self->loc("Member not deleted" ));
1302 if ($self->RoleClass) {
1304 OldValue => $old_member,
1305 Field => $self->Name,
1308 if ($self->SingleMemberRoleGroup) {
1309 # _AddMember creates the Set-Owner txn (for example) but we handle
1310 # the SetWatcher-Owner txn below.
1312 PrincipalId => RT->Nobody->Id,
1313 RecordTransaction => 0,
1314 RecordSetTransaction => $args{RecordTransaction},
1316 $txn{Type} = "SetWatcher";
1317 $txn{NewValue} = RT->Nobody->id;
1319 $txn{Type} = "DelWatcher";
1322 if ($args{RecordTransaction}) {
1323 my $obj = $args{Object} || $self->RoleGroupObject;
1324 $obj->_NewTransaction(%txn);
1328 # Record transactions for UserDefined groups
1329 if ($args{RecordTransaction} && $self->Domain eq 'UserDefined') {
1330 $member_obj->MemberObj->Object->_NewTransaction(
1331 Type => 'DeleteMembership',
1332 Field => $self->PrincipalObj->id,
1335 $self->_NewTransaction(
1336 Type => 'DeleteMember',
1337 Field => $member_id,
1341 return ( $val, $self->loc("Member deleted") );
1351 TransactionType => 'Set',
1352 RecordTransaction => 1,
1356 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1357 return ( 0, $self->loc('Permission Denied') );
1360 my $Old = $self->SUPER::_Value("$args{'Field'}");
1362 my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1363 Value => $args{'Value'} );
1365 #If we can't actually set the field to the value, don't record
1366 # a transaction. instead, get out of here.
1367 if ( $ret == 0 ) { return ( 0, $msg ); }
1369 if ( $args{'RecordTransaction'} == 1 ) {
1371 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1372 Type => $args{'TransactionType'},
1373 Field => $args{'Field'},
1374 NewValue => $args{'Value'},
1376 TimeTaken => $args{'TimeTaken'},
1378 return ( $Trans, scalar $TransObj->Description );
1381 return ( $ret, $msg );
1385 =head2 CurrentUserCanSee
1387 Always returns 1; unfortunately, for historical reasons, users have
1388 always been able to examine groups they have indirect access to, even if
1389 they do not have SeeGroup explicitly.
1393 sub CurrentUserCanSee {
1401 Returns the principal object for this user. returns an empty RT::Principal
1402 if there's no principal object matching this user.
1403 The response is cached. PrincipalObj should never ever change.
1411 my $res = RT::Principal->new( $self->CurrentUser );
1412 $res->Load( $self->id );
1419 Returns this user's PrincipalId
1432 if ( $self->Domain eq 'ACLEquivalence' ) {
1433 $class = "RT::User";
1434 } elsif ($self->Domain eq 'RT::Queue-Role') {
1435 $class = "RT::Queue";
1436 } elsif ($self->Domain eq 'RT::Ticket-Role') {
1437 $class = "RT::Ticket";
1440 return unless $class;
1442 my $obj = $class->new( $self->CurrentUser );
1443 $obj->Load( $self->Instance );
1450 [ Description => 'Description' ],
1457 Jesse Vincent, jesse@bestpractical.com
1471 Returns the current value of id.
1472 (In the database, id is stored as int(11).)
1480 Returns the current value of Name.
1481 (In the database, Name is stored as varchar(200).)
1485 =head2 SetName VALUE
1489 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1490 (In the database, Name will be stored as a varchar(200).)
1498 Returns the current value of Description.
1499 (In the database, Description is stored as varchar(255).)
1503 =head2 SetDescription VALUE
1506 Set Description to VALUE.
1507 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1508 (In the database, Description will be stored as a varchar(255).)
1516 Returns the current value of Domain.
1517 (In the database, Domain is stored as varchar(64).)
1521 =head2 SetDomain VALUE
1524 Set Domain to VALUE.
1525 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1526 (In the database, Domain will be stored as a varchar(64).)
1534 Returns the current value of Type.
1535 (In the database, Type is stored as varchar(64).)
1537 Deprecated, use Name instead, will be removed in 4.4.
1539 =head2 SetType VALUE
1543 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1544 (In the database, Type will be stored as a varchar(64).)
1546 Deprecated, use SetName instead, will be removed in 4.4.
1553 Returns the current value of Instance.
1554 (In the database, Instance is stored as int(11).)
1558 =head2 SetInstance VALUE
1561 Set Instance to VALUE.
1562 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1563 (In the database, Instance will be stored as a int(11).)
1571 Returns the current value of Creator.
1572 (In the database, Creator is stored as int(11).)
1580 Returns the current value of Created.
1581 (In the database, Created is stored as datetime.)
1587 =head2 LastUpdatedBy
1589 Returns the current value of LastUpdatedBy.
1590 (In the database, LastUpdatedBy is stored as int(11).)
1598 Returns the current value of LastUpdated.
1599 (In the database, LastUpdated is stored as datetime.)
1606 sub _CoreAccessible {
1610 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1612 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
1614 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1616 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1618 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1620 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1622 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1624 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1626 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1628 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1633 sub FindDependencies {
1635 my ($walker, $deps) = @_;
1637 $self->SUPER::FindDependencies($walker, $deps);
1639 my $instance = $self->InstanceObj;
1640 $deps->Add( out => $instance ) if $instance;
1642 # Group members records, unless we're a system group
1643 if ($self->Domain ne "SystemInternal") {
1644 my $objs = RT::GroupMembers->new( $self->CurrentUser );
1645 $objs->LimitToMembersOfGroup( $self->PrincipalId );
1646 $deps->Add( in => $objs );
1649 # Group member records group belongs to
1650 my $objs = RT::GroupMembers->new( $self->CurrentUser );
1651 $objs->Limit( FIELD => 'MemberId', VALUE => $self->PrincipalId );
1652 $deps->Add( in => $objs );
1659 Dependencies => undef,
1662 my $deps = $args{'Dependencies'};
1665 # User is inconsistent without own Equivalence group
1666 if( $self->Domain eq 'ACLEquivalence' ) {
1667 # delete user entry after ACL equiv group
1668 # in other case we will get deep recursion
1669 my $objs = RT::User->new($self->CurrentUser);
1670 $objs->Load( $self->Instance );
1671 $deps->_PushDependency(
1672 BaseObject => $self,
1673 Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER,
1674 TargetObject => $objs,
1675 Shredder => $args{'Shredder'}
1680 $deps->_PushDependency(
1681 BaseObject => $self,
1682 Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER,
1683 TargetObject => $self->PrincipalObj,
1684 Shredder => $args{'Shredder'}
1687 # Group members records
1688 my $objs = RT::GroupMembers->new( $self->CurrentUser );
1689 $objs->LimitToMembersOfGroup( $self->PrincipalId );
1690 push( @$list, $objs );
1692 # Group member records group belongs to
1693 $objs = RT::GroupMembers->new( $self->CurrentUser );
1695 VALUE => $self->PrincipalId,
1696 FIELD => 'MemberId',
1697 ENTRYAGGREGATOR => 'OR',
1700 push( @$list, $objs );
1702 # Cached group members records
1703 push( @$list, $self->DeepMembersObj );
1705 # Cached group member records group belongs to
1706 $objs = RT::GroupMembers->new( $self->CurrentUser );
1708 VALUE => $self->PrincipalId,
1709 FIELD => 'MemberId',
1710 ENTRYAGGREGATOR => 'OR',
1713 push( @$list, $objs );
1715 # Cleanup group's membership transactions
1716 $objs = RT::Transactions->new( $self->CurrentUser );
1717 $objs->Limit( FIELD => 'Type', OPERATOR => 'IN', VALUE => ['AddMember', 'DeleteMember'] );
1718 $objs->Limit( FIELD => 'Field', VALUE => $self->PrincipalObj->id, ENTRYAGGREGATOR => 'AND' );
1719 push( @$list, $objs );
1721 $deps->_PushDependencies(
1722 BaseObject => $self,
1723 Flags => RT::Shredder::Constants::DEPENDS_ON,
1724 TargetObjects => $list,
1725 Shredder => $args{'Shredder'}
1727 return $self->SUPER::__DependsOn( %args );
1732 if( $self->Domain eq 'SystemInternal' ) {
1733 RT::Shredder::Exception::Info->throw('SystemObject');
1735 return $self->SUPER::BeforeWipeout( @_ );
1741 my %store = $self->SUPER::Serialize(@_);
1743 my $instance = $self->InstanceObj;
1744 $store{Instance} = \($instance->UID) if $instance;
1746 $store{Disabled} = $self->PrincipalObj->Disabled;
1747 $store{Principal} = $self->PrincipalObj->UID;
1748 $store{PrincipalId} = $self->PrincipalObj->Id;
1754 my ($importer, $uid, $data) = @_;
1756 my $principal_uid = delete $data->{Principal};
1757 my $principal_id = delete $data->{PrincipalId};
1758 my $disabled = delete $data->{Disabled};
1760 # Inflate refs into their IDs
1761 $class->SUPER::PreInflate( $importer, $uid, $data );
1763 # Factored out code, in case we find an existing version of this group
1764 my $obj = RT::Group->new( RT->SystemUser );
1765 my $duplicated = sub {
1766 $importer->SkipTransactions( $uid );
1769 ref($obj->PrincipalObj),
1770 $obj->PrincipalObj->Id
1772 $importer->Resolve( $uid => ref($obj), $obj->Id );
1776 # Go looking for the pre-existing version of it
1777 if ($data->{Domain} eq "ACLEquivalence") {
1778 $obj->LoadACLEquivalenceGroup( $data->{Instance} );
1779 return $duplicated->() if $obj->Id;
1781 # Update description for the new ID
1782 $data->{Description} = 'ACL equiv. for user '.$data->{Instance};
1783 } elsif ($data->{Domain} eq "UserDefined") {
1784 $data->{Name} = $importer->Qualify($data->{Name});
1785 $obj->LoadUserDefinedGroup( $data->{Name} );
1787 $importer->MergeValues($obj, $data);
1788 return $duplicated->();
1790 } elsif ($data->{Domain} =~ /^(SystemInternal|RT::System-Role)$/) {
1791 $obj->LoadByCols( Domain => $data->{Domain}, Name => $data->{Name} );
1792 return $duplicated->() if $obj->Id;
1793 } elsif ($data->{Domain} eq "RT::Queue-Role") {
1794 my $queue = RT::Queue->new( RT->SystemUser );
1795 $queue->Load( $data->{Instance} );
1796 $obj->LoadRoleGroup( Object => $queue, Name => $data->{Name} );
1797 return $duplicated->() if $obj->Id;
1800 my $principal = RT::Principal->new( RT->SystemUser );
1801 my ($id) = $principal->Create(
1802 PrincipalType => 'Group',
1803 Disabled => $disabled,
1807 # Now we have a principal id, set the id for the group record
1810 $importer->Resolve( $principal_uid => ref($principal), $id );
1812 $importer->Postpone(
1814 uid => $principal_uid,
1815 column => "ObjectId",
1824 my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
1826 Group => $self->PrincipalObj,
1827 Member => $self->PrincipalObj,
1828 ImmediateParent => $self->PrincipalObj
1832 RT::Base->_ImportOverlays();