2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2016 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 an Add/SetWatcher txn on the object if we're a role group
1111 if ($args{RecordTransaction} and $self->RoleClass) {
1112 my $obj = $args{Object} || $self->RoleGroupObject;
1114 if ($self->SingleMemberRoleGroup) {
1115 $obj->_NewTransaction(
1116 Type => 'SetWatcher',
1117 OldValue => $old_member_id,
1118 NewValue => $new_member_obj->Id,
1119 Field => $self->Name,
1122 $obj->_NewTransaction(
1123 Type => 'AddWatcher', # use "watcher" for history's sake
1124 NewValue => $new_member_obj->Id,
1125 Field => $self->Name,
1130 return (1, $self->loc("[_1] set to [_2]",
1131 $self->loc($self->Name), $new_member_obj->Object->Name) )
1132 if $self->SingleMemberRoleGroup;
1134 return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
1138 =head2 HasMember RT::Principal|id
1140 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a
1141 member of this group.
1142 Returns undef if the user isn't a member of the group or if the current
1143 user doesn't have permission to find out. Arguably, it should differentiate
1144 between ACL failure and non membership.
1150 my $principal = shift;
1153 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1154 $id = $principal->id;
1155 } elsif ( $principal =~ /^\d+$/ ) {
1158 $RT::Logger->error("Group::HasMember was called with an argument that".
1159 " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
1162 return undef unless $id;
1164 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1165 $member_obj->LoadByCols(
1167 GroupId => $self->PrincipalId
1170 if ( my $member_id = $member_obj->id ) {
1180 =head2 HasMemberRecursively RT::Principal|id
1182 Takes an L<RT::Principal> object or its id and returns true if that user is a member of
1184 Returns undef if the user isn't a member of the group or if the current
1185 user doesn't have permission to find out. Arguably, it should differentiate
1186 between ACL failure and non membership.
1190 sub HasMemberRecursively {
1192 my $principal = shift;
1195 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1196 $id = $principal->id;
1197 } elsif ( $principal =~ /^\d+$/ ) {
1200 $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1201 " isn't an RT::Principal or id. It's $principal");
1204 return undef unless $id;
1206 my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1207 $member_obj->LoadByCols(
1209 GroupId => $self->PrincipalId
1212 if ( my $member_id = $member_obj->id ) {
1222 =head2 DeleteMember PRINCIPAL_ID
1224 Takes the principal id of a current user or group.
1225 If the current user has apropriate rights,
1226 removes that GroupMember from this group.
1227 Returns a two value array. the first value is true on successful
1228 addition or 0 on failure. The second value is a textual status msg.
1230 Optionally takes a hash of key value flags, such as RecordTransaction.
1236 my $member_id = shift;
1239 # We should only allow membership changes if the user has the right
1240 # to modify group membership or the user is the principal in question
1241 # and the user has the right to modify his own membership
1243 unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1244 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1245 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1246 #User has no permission to be doing this
1247 return ( 0, $self->loc("Permission Denied") );
1249 $self->_DeleteMember($member_id, @_);
1252 # A helper subroutine for DeleteMember that bypasses the ACL checks
1253 # this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
1254 # when we want to deal with groups according to queue rights
1255 # In the dim future, this will all get factored out and life
1260 my $member_id = shift;
1262 RecordTransaction => 1,
1267 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1269 $member_obj->LoadByCols( MemberId => $member_id,
1270 GroupId => $self->PrincipalId);
1273 #If we couldn't load it, return undef.
1274 unless ( $member_obj->Id() ) {
1275 $RT::Logger->debug("Group has no member with that id");
1276 return ( 0,$self->loc( "Group has no such member" ));
1279 my $old_member = $member_obj->MemberId;
1281 #Now that we've checked ACLs and sanity, delete the groupmember
1282 my $val = $member_obj->Delete();
1285 $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1286 return ( 0, $self->loc("Member not deleted" ));
1289 if ($self->RoleClass) {
1291 OldValue => $old_member,
1292 Field => $self->Name,
1295 if ($self->SingleMemberRoleGroup) {
1296 # _AddMember creates the Set-Owner txn (for example) but we handle
1297 # the SetWatcher-Owner txn below.
1299 PrincipalId => RT->Nobody->Id,
1300 RecordTransaction => 0,
1301 RecordSetTransaction => $args{RecordTransaction},
1303 $txn{Type} = "SetWatcher";
1304 $txn{NewValue} = RT->Nobody->id;
1306 $txn{Type} = "DelWatcher";
1309 if ($args{RecordTransaction}) {
1310 my $obj = $args{Object} || $self->RoleGroupObject;
1311 $obj->_NewTransaction(%txn);
1315 return ( $val, $self->loc("Member deleted") );
1325 TransactionType => 'Set',
1326 RecordTransaction => 1,
1330 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1331 return ( 0, $self->loc('Permission Denied') );
1334 my $Old = $self->SUPER::_Value("$args{'Field'}");
1336 my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1337 Value => $args{'Value'} );
1339 #If we can't actually set the field to the value, don't record
1340 # a transaction. instead, get out of here.
1341 if ( $ret == 0 ) { return ( 0, $msg ); }
1343 if ( $args{'RecordTransaction'} == 1 ) {
1345 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1346 Type => $args{'TransactionType'},
1347 Field => $args{'Field'},
1348 NewValue => $args{'Value'},
1350 TimeTaken => $args{'TimeTaken'},
1352 return ( $Trans, scalar $TransObj->Description );
1355 return ( $ret, $msg );
1359 =head2 CurrentUserCanSee
1361 Always returns 1; unfortunately, for historical reasons, users have
1362 always been able to examine groups they have indirect access to, even if
1363 they do not have SeeGroup explicitly.
1367 sub CurrentUserCanSee {
1375 Returns the principal object for this user. returns an empty RT::Principal
1376 if there's no principal object matching this user.
1377 The response is cached. PrincipalObj should never ever change.
1385 my $res = RT::Principal->new( $self->CurrentUser );
1386 $res->Load( $self->id );
1393 Returns this user's PrincipalId
1406 if ( $self->Domain eq 'ACLEquivalence' ) {
1407 $class = "RT::User";
1408 } elsif ($self->Domain eq 'RT::Queue-Role') {
1409 $class = "RT::Queue";
1410 } elsif ($self->Domain eq 'RT::Ticket-Role') {
1411 $class = "RT::Ticket";
1414 return unless $class;
1416 my $obj = $class->new( $self->CurrentUser );
1417 $obj->Load( $self->Instance );
1424 [ Description => 'Description' ],
1431 Jesse Vincent, jesse@bestpractical.com
1445 Returns the current value of id.
1446 (In the database, id is stored as int(11).)
1454 Returns the current value of Name.
1455 (In the database, Name is stored as varchar(200).)
1459 =head2 SetName VALUE
1463 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1464 (In the database, Name will be stored as a varchar(200).)
1472 Returns the current value of Description.
1473 (In the database, Description is stored as varchar(255).)
1477 =head2 SetDescription VALUE
1480 Set Description to VALUE.
1481 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1482 (In the database, Description will be stored as a varchar(255).)
1490 Returns the current value of Domain.
1491 (In the database, Domain is stored as varchar(64).)
1495 =head2 SetDomain VALUE
1498 Set Domain to VALUE.
1499 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1500 (In the database, Domain will be stored as a varchar(64).)
1508 Returns the current value of Type.
1509 (In the database, Type is stored as varchar(64).)
1511 Deprecated, use Name instead, will be removed in 4.4.
1513 =head2 SetType VALUE
1517 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1518 (In the database, Type will be stored as a varchar(64).)
1520 Deprecated, use SetName instead, will be removed in 4.4.
1527 Returns the current value of Instance.
1528 (In the database, Instance is stored as int(11).)
1532 =head2 SetInstance VALUE
1535 Set Instance to VALUE.
1536 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1537 (In the database, Instance will be stored as a int(11).)
1545 Returns the current value of Creator.
1546 (In the database, Creator is stored as int(11).)
1554 Returns the current value of Created.
1555 (In the database, Created is stored as datetime.)
1561 =head2 LastUpdatedBy
1563 Returns the current value of LastUpdatedBy.
1564 (In the database, LastUpdatedBy is stored as int(11).)
1572 Returns the current value of LastUpdated.
1573 (In the database, LastUpdated is stored as datetime.)
1580 sub _CoreAccessible {
1584 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1586 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
1588 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1590 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1592 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1594 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1596 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1598 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1600 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1602 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1607 sub FindDependencies {
1609 my ($walker, $deps) = @_;
1611 $self->SUPER::FindDependencies($walker, $deps);
1613 my $instance = $self->InstanceObj;
1614 $deps->Add( out => $instance ) if $instance;
1616 # Group members records, unless we're a system group
1617 if ($self->Domain ne "SystemInternal") {
1618 my $objs = RT::GroupMembers->new( $self->CurrentUser );
1619 $objs->LimitToMembersOfGroup( $self->PrincipalId );
1620 $deps->Add( in => $objs );
1623 # Group member records group belongs to
1624 my $objs = RT::GroupMembers->new( $self->CurrentUser );
1625 $objs->Limit( FIELD => 'MemberId', VALUE => $self->PrincipalId );
1626 $deps->Add( in => $objs );
1633 Dependencies => undef,
1636 my $deps = $args{'Dependencies'};
1639 # User is inconsistent without own Equivalence group
1640 if( $self->Domain eq 'ACLEquivalence' ) {
1641 # delete user entry after ACL equiv group
1642 # in other case we will get deep recursion
1643 my $objs = RT::User->new($self->CurrentUser);
1644 $objs->Load( $self->Instance );
1645 $deps->_PushDependency(
1646 BaseObject => $self,
1647 Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER,
1648 TargetObject => $objs,
1649 Shredder => $args{'Shredder'}
1654 $deps->_PushDependency(
1655 BaseObject => $self,
1656 Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER,
1657 TargetObject => $self->PrincipalObj,
1658 Shredder => $args{'Shredder'}
1661 # Group members records
1662 my $objs = RT::GroupMembers->new( $self->CurrentUser );
1663 $objs->LimitToMembersOfGroup( $self->PrincipalId );
1664 push( @$list, $objs );
1666 # Group member records group belongs to
1667 $objs = RT::GroupMembers->new( $self->CurrentUser );
1669 VALUE => $self->PrincipalId,
1670 FIELD => 'MemberId',
1671 ENTRYAGGREGATOR => 'OR',
1674 push( @$list, $objs );
1676 # Cached group members records
1677 push( @$list, $self->DeepMembersObj );
1679 # Cached group member records group belongs to
1680 $objs = RT::GroupMembers->new( $self->CurrentUser );
1682 VALUE => $self->PrincipalId,
1683 FIELD => 'MemberId',
1684 ENTRYAGGREGATOR => 'OR',
1687 push( @$list, $objs );
1689 $deps->_PushDependencies(
1690 BaseObject => $self,
1691 Flags => RT::Shredder::Constants::DEPENDS_ON,
1692 TargetObjects => $list,
1693 Shredder => $args{'Shredder'}
1695 return $self->SUPER::__DependsOn( %args );
1700 if( $self->Domain eq 'SystemInternal' ) {
1701 RT::Shredder::Exception::Info->throw('SystemObject');
1703 return $self->SUPER::BeforeWipeout( @_ );
1709 my %store = $self->SUPER::Serialize(@_);
1711 my $instance = $self->InstanceObj;
1712 $store{Instance} = \($instance->UID) if $instance;
1714 $store{Disabled} = $self->PrincipalObj->Disabled;
1715 $store{Principal} = $self->PrincipalObj->UID;
1716 $store{PrincipalId} = $self->PrincipalObj->Id;
1722 my ($importer, $uid, $data) = @_;
1724 my $principal_uid = delete $data->{Principal};
1725 my $principal_id = delete $data->{PrincipalId};
1726 my $disabled = delete $data->{Disabled};
1728 # Inflate refs into their IDs
1729 $class->SUPER::PreInflate( $importer, $uid, $data );
1731 # Factored out code, in case we find an existing version of this group
1732 my $obj = RT::Group->new( RT->SystemUser );
1733 my $duplicated = sub {
1734 $importer->SkipTransactions( $uid );
1737 ref($obj->PrincipalObj),
1738 $obj->PrincipalObj->Id
1740 $importer->Resolve( $uid => ref($obj), $obj->Id );
1744 # Go looking for the pre-existing version of the it
1745 if ($data->{Domain} eq "ACLEquivalence") {
1746 $obj->LoadACLEquivalenceGroup( $data->{Instance} );
1747 return $duplicated->() if $obj->Id;
1749 # Update the name and description for the new ID
1750 $data->{Name} = 'User '. $data->{Instance};
1751 $data->{Description} = 'ACL equiv. for user '.$data->{Instance};
1752 } elsif ($data->{Domain} eq "UserDefined") {
1753 $data->{Name} = $importer->Qualify($data->{Name});
1754 $obj->LoadUserDefinedGroup( $data->{Name} );
1756 $importer->MergeValues($obj, $data);
1757 return $duplicated->();
1759 } elsif ($data->{Domain} =~ /^(SystemInternal|RT::System-Role)$/) {
1760 $obj->LoadByCols( Domain => $data->{Domain}, Name => $data->{Name} );
1761 return $duplicated->() if $obj->Id;
1762 } elsif ($data->{Domain} eq "RT::Queue-Role") {
1763 my $queue = RT::Queue->new( RT->SystemUser );
1764 $queue->Load( $data->{Instance} );
1765 $obj->LoadRoleGroup( Object => $queue, Name => $data->{Name} );
1766 return $duplicated->() if $obj->Id;
1769 my $principal = RT::Principal->new( RT->SystemUser );
1770 my ($id) = $principal->Create(
1771 PrincipalType => 'Group',
1772 Disabled => $disabled,
1776 # Now we have a principal id, set the id for the group record
1779 $importer->Resolve( $principal_uid => ref($principal), $id );
1781 $importer->Postpone(
1783 uid => $principal_uid,
1784 column => "ObjectId",
1793 my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
1795 Group => $self->PrincipalObj,
1796 Member => $self->PrincipalObj,
1797 ImmediateParent => $self->PrincipalObj
1801 RT::Base->_ImportOverlays();