]> git.uio.no Git - usit-rt.git/blame - lib/RT/Group.pm
Upgrade to 4.0.13
[usit-rt.git] / lib / RT / Group.pm
CommitLineData
84fb5b46
MKG
1
2# BEGIN BPS TAGGED BLOCK {{{
3#
4# COPYRIGHT:
5#
403d7b0b 6# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
84fb5b46
MKG
7# <sales@bestpractical.com>
8#
9# (Except where explicitly superseded by other copyright notices)
10#
11#
12# LICENSE:
13#
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
17# from www.gnu.org.
18#
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.
23#
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.
29#
30#
31# CONTRIBUTION SUBMISSION POLICY:
32#
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.)
38#
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.
47#
48# END BPS TAGGED BLOCK }}}
49
50# Released under the terms of version 2 of the GNU Public License
51
52=head1 NAME
53
403d7b0b 54 RT::Group - RT's group object
84fb5b46
MKG
55
56=head1 SYNOPSIS
57
58use RT::Group;
59my $group = RT::Group->new($CurrentUser);
60
61=head1 DESCRIPTION
62
63An RT group object.
64
65=head1 METHODS
66
67
68
69
70
71=cut
72
73
74package RT::Group;
75
76
77use strict;
78use warnings;
79
80use base 'RT::Record';
81
82sub Table {'Groups'}
83
84
85
86use RT::Users;
87use RT::GroupMembers;
88use RT::Principals;
89use RT::ACL;
90
91use vars qw/$RIGHTS $RIGHT_CATEGORIES/;
92
93$RIGHTS = {
94 AdminGroup => 'Modify group metadata or delete group', # loc_pair
95 AdminGroupMembership => 'Modify group membership roster', # loc_pair
96 ModifyOwnMembership => 'Join or leave group', # loc_pair
97 EditSavedSearches => 'Create, modify and delete saved searches', # loc_pair
98 ShowSavedSearches => 'View saved searches', # loc_pair
99 SeeGroup => 'View group', # loc_pair
100 SeeGroupDashboard => 'View group dashboards', # loc_pair
101 CreateGroupDashboard => 'Create group dashboards', # loc_pair
102 ModifyGroupDashboard => 'Modify group dashboards', # loc_pair
103 DeleteGroupDashboard => 'Delete group dashboards', # loc_pair
104};
105
106$RIGHT_CATEGORIES = {
107 AdminGroup => 'Admin',
108 AdminGroupMembership => 'Admin',
109 ModifyOwnMembership => 'Staff',
110 EditSavedSearches => 'Admin',
111 ShowSavedSearches => 'Staff',
112 SeeGroup => 'Staff',
113 SeeGroupDashboard => 'Staff',
114 CreateGroupDashboard => 'Admin',
115 ModifyGroupDashboard => 'Admin',
116 DeleteGroupDashboard => 'Admin',
117};
118
119# Tell RT::ACE that this sort of object can get acls granted
120$RT::ACE::OBJECT_TYPES{'RT::Group'} = 1;
121
122
123#
124
125# TODO: This should be refactored out into an RT::ACLedObject or something
126# stuff the rights into a hash of rights that can exist.
127
128__PACKAGE__->AddRights(%$RIGHTS);
129__PACKAGE__->AddRightCategories(%$RIGHT_CATEGORIES);
130
131=head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
132
133Adds the given rights to the list of possible rights. This method
134should be called during server startup, not at runtime.
135
136=cut
137
138sub AddRights {
139 my $self = shift;
140 my %new = @_;
141 $RIGHTS = { %$RIGHTS, %new };
142 %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
143 map { lc($_) => $_ } keys %new);
144}
145
146=head2 AvailableRights
147
148Returns 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
149
150=cut
151
152sub AvailableRights {
153 my $self = shift;
154 return($RIGHTS);
155}
156
157=head2 RightCategories
158
159Returns a hashref where the keys are rights for this type of object and the
160values are the category (General, Staff, Admin) the right falls into.
161
162=cut
163
164sub RightCategories {
165 return $RIGHT_CATEGORIES;
166}
167
168=head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...]
169
170Adds the given right and category pairs to the list of right categories. This
171method should be called during server startup, not at runtime.
172
173=cut
174
175sub AddRightCategories {
176 my $self = shift if ref $_[0] or $_[0] eq __PACKAGE__;
177 my %new = @_;
178 $RIGHT_CATEGORIES = { %$RIGHT_CATEGORIES, %new };
179}
180
181
182
183=head2 SelfDescription
184
185Returns a user-readable description of what this group is for and what it's named.
186
187=cut
188
189sub SelfDescription {
190 my $self = shift;
191 if ($self->Domain eq 'ACLEquivalence') {
192 my $user = RT::Principal->new($self->CurrentUser);
193 $user->Load($self->Instance);
194 return $self->loc("user [_1]",$user->Object->Name);
195 }
196 elsif ($self->Domain eq 'UserDefined') {
197 return $self->loc("group '[_1]'",$self->Name);
198 }
199 elsif ($self->Domain eq 'RT::System-Role') {
200 return $self->loc("system [_1]",$self->Type);
201 }
202 elsif ($self->Domain eq 'RT::Queue-Role') {
203 my $queue = RT::Queue->new($self->CurrentUser);
204 $queue->Load($self->Instance);
205 return $self->loc("queue [_1] [_2]",$queue->Name, $self->Type);
206 }
207 elsif ($self->Domain eq 'RT::Ticket-Role') {
208 return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Type);
209 }
210 elsif ($self->Domain eq 'SystemInternal') {
211 return $self->loc("system group '[_1]'",$self->Type);
212 }
213 else {
214 return $self->loc("undescribed group [_1]",$self->Id);
215 }
216}
217
218
219
220=head2 Load ID
221
222Load a group object from the database. Takes a single argument.
223If the argument is numerical, load by the column 'id'. Otherwise,
224complain and return.
225
226=cut
227
228sub Load {
229 my $self = shift;
230 my $identifier = shift || return undef;
231
232 if ( $identifier !~ /\D/ ) {
233 $self->SUPER::LoadById($identifier);
234 }
235 else {
236 $RT::Logger->crit("Group -> Load called with a bogus argument");
237 return undef;
238 }
239}
240
241
242
243=head2 LoadUserDefinedGroup NAME
244
245Loads a system group from the database. The only argument is
246the group's name.
247
248
249=cut
250
251sub LoadUserDefinedGroup {
252 my $self = shift;
253 my $identifier = shift;
254
255 if ( $identifier =~ /^\d+$/ ) {
256 return $self->LoadByCols(
257 Domain => 'UserDefined',
258 id => $identifier,
259 );
260 } else {
261 return $self->LoadByCols(
262 Domain => 'UserDefined',
263 Name => $identifier,
264 );
265 }
266}
267
268
269
270=head2 LoadACLEquivalenceGroup PRINCIPAL
271
272Loads a user's acl equivalence group. Takes a principal object or its ID.
273ACL equivalnce groups are used to simplify the acl system. Each user
274has one group that only he is a member of. Rights granted to the user
275are actually granted to that group. This greatly simplifies ACL checks.
276While this results in a somewhat more complex setup when creating users
277and granting ACLs, it _greatly_ simplifies acl checks.
278
279=cut
280
281sub LoadACLEquivalenceGroup {
282 my $self = shift;
283 my $principal = shift;
284 $principal = $principal->id if ref $principal;
285
286 return $self->LoadByCols(
287 Domain => 'ACLEquivalence',
288 Type => 'UserEquiv',
289 Instance => $principal,
290 );
291}
292
293
294
295
296=head2 LoadSystemInternalGroup NAME
297
298Loads a Pseudo group from the database. The only argument is
299the group's name.
300
301
302=cut
303
304sub LoadSystemInternalGroup {
305 my $self = shift;
306 my $identifier = shift;
307
308 return $self->LoadByCols(
309 Domain => 'SystemInternal',
310 Type => $identifier,
311 );
312}
313
314
315
316=head2 LoadTicketRoleGroup { Ticket => TICKET_ID, Type => TYPE }
317
318Loads a ticket group from the database.
319
320Takes a param hash with 2 parameters:
321
322 Ticket is the TicketId we're curious about
323 Type is the type of Group we're trying to load:
324 Requestor, Cc, AdminCc, Owner
325
326=cut
327
328sub LoadTicketRoleGroup {
329 my $self = shift;
330 my %args = (Ticket => '0',
331 Type => undef,
332 @_);
333 $self->LoadByCols( Domain => 'RT::Ticket-Role',
334 Instance =>$args{'Ticket'},
335 Type => $args{'Type'}
336 );
337}
338
339
340
341=head2 LoadQueueRoleGroup { Queue => Queue_ID, Type => TYPE }
342
343Loads a Queue group from the database.
344
345Takes a param hash with 2 parameters:
346
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
350
351=cut
352
353sub LoadQueueRoleGroup {
354 my $self = shift;
355 my %args = (Queue => undef,
356 Type => undef,
357 @_);
358 $self->LoadByCols( Domain => 'RT::Queue-Role',
359 Instance =>$args{'Queue'},
360 Type => $args{'Type'}
361 );
362}
363
364
365
366=head2 LoadSystemRoleGroup Type
367
368Loads a System group from the database.
369
370Takes a single param: Type
371
372 Type is the type of Group we're trying to load:
373 Requestor, Cc, AdminCc, Owner
374
375=cut
376
377sub LoadSystemRoleGroup {
378 my $self = shift;
379 my $type = shift;
380 $self->LoadByCols( Domain => 'RT::System-Role',
381 Type => $type
382 );
383}
384
385
386
387=head2 Create
388
389You need to specify what sort of group you're creating by calling one of the other
390Create_____ routines.
391
392=cut
393
394sub Create {
395 my $self = shift;
396 $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
397 return(0,$self->loc('Permission Denied'));
398}
399
400
401
402=head2 _Create
403
404Takes a paramhash with named arguments: Name, Description.
405
406Returns a tuple of (Id, Message). If id is 0, the create failed
407
408=cut
409
410sub _Create {
411 my $self = shift;
412 my %args = (
413 Name => undef,
414 Description => undef,
415 Domain => undef,
416 Type => undef,
417 Instance => '0',
418 InsideTransaction => undef,
419 _RecordTransaction => 1,
420 @_
421 );
422
423 # Enforce uniqueness on user defined group names
424 if ($args{'Domain'} and $args{'Domain'} eq 'UserDefined') {
425 my ($ok, $msg) = $self->_ValidateUserDefinedName($args{'Name'});
426 return ($ok, $msg) if not $ok;
427 }
428
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',
435 ObjectId => '0'
436 );
437 $principal->__Set(Field => 'ObjectId', Value => $principal_id);
438
439 $self->SUPER::Create(
440 id => $principal_id,
441 Name => $args{'Name'},
442 Description => $args{'Description'},
443 Type => $args{'Type'},
444 Domain => $args{'Domain'},
445 Instance => ($args{'Instance'} || '0')
446 );
447 my $id = $self->Id;
448 unless ($id) {
449 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
450 return ( 0, $self->loc('Could not create group') );
451 }
452
453 # If we couldn't create a principal Id, get the fuck out.
454 unless ($principal_id) {
455 $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
456 $RT::Logger->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
457 return ( 0, $self->loc('Could not create group') );
458 }
459
460 # Now we make the group a member of itself as a cached group member
461 # this needs to exist so that group ACL checks don't fall over.
462 # you're checking CachedGroupMembers to see if the principal in question
463 # is a member of the principal the rights have been granted too
464
465 # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as
466 # cached members. thankfully, we're creating the group now...so it has no members.
467 my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
468 $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
469
470
471 if ( $args{'_RecordTransaction'} ) {
472 $self->_NewTransaction( Type => "Create" );
473 }
474
475 $RT::Handle->Commit() unless ($args{'InsideTransaction'});
476
477 return ( $id, $self->loc("Group created") );
478}
479
480
481
482=head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
483
484A helper subroutine which creates a system group
485
486Returns a tuple of (Id, Message). If id is 0, the create failed
487
488=cut
489
490sub CreateUserDefinedGroup {
491 my $self = shift;
492
493 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
494 $RT::Logger->warning( $self->CurrentUser->Name
495 . " Tried to create a group without permission." );
496 return ( 0, $self->loc('Permission Denied') );
497 }
498
499 return($self->_Create( Domain => 'UserDefined', Type => '', Instance => '', @_));
500}
501
502=head2 ValidateName VALUE
503
504Enforces unique user defined group names when updating
505
506=cut
507
508sub ValidateName {
509 my ($self, $value) = @_;
510
511 if ($self->Domain and $self->Domain eq 'UserDefined') {
512 my ($ok, $msg) = $self->_ValidateUserDefinedName($value);
513 # It's really too bad we can't pass along the actual error
514 return 0 if not $ok;
515 }
516 return $self->SUPER::ValidateName($value);
517}
518
519=head2 _ValidateUserDefinedName VALUE
520
521Returns true if the user defined group name isn't in use, false otherwise.
522
523=cut
524
525sub _ValidateUserDefinedName {
526 my ($self, $value) = @_;
527
528 return (0, 'Name is required') unless length $value;
529
530 my $dupcheck = RT::Group->new(RT->SystemUser);
531 $dupcheck->LoadUserDefinedGroup($value);
5b0d0914
MKG
532 if ( $dupcheck->id && ( !$self->id || $self->id != $dupcheck->id ) ) {
533 return ( 0, $self->loc( "Group name '[_1]' is already in use", $value ) );
534 }
84fb5b46
MKG
535 return 1;
536}
537
538=head2 _CreateACLEquivalenceGroup { Principal }
539
540A helper subroutine which creates a group containing only
541an individual user. This gets used by the ACL system to check rights.
542Yes, it denormalizes the data, but that's ok, as we totally win on performance.
543
544Returns a tuple of (Id, Message). If id is 0, the create failed
545
546=cut
547
548sub _CreateACLEquivalenceGroup {
549 my $self = shift;
550 my $princ = shift;
551
552 my $id = $self->_Create( Domain => 'ACLEquivalence',
553 Type => 'UserEquiv',
554 Name => 'User '. $princ->Object->Id,
555 Description => 'ACL equiv. for user '.$princ->Object->Id,
556 Instance => $princ->Id,
557 InsideTransaction => 1,
558 _RecordTransaction => 0 );
559 unless ($id) {
560 $RT::Logger->crit("Couldn't create ACL equivalence group");
561 return undef;
562 }
563
564 # We use stashuser so we don't get transactions inside transactions
565 # and so we bypass all sorts of cruft we don't need
566 my $aclstash = RT::GroupMember->new($self->CurrentUser);
567 my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
568 Member => $princ);
569
570 unless ($stash_id) {
571 $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
572 # We call super delete so we don't get acl checked.
573 $self->SUPER::Delete();
574 return(undef);
575 }
576 return ($id);
577}
578
579
580
581
582=head2 CreateRoleGroup { Domain => DOMAIN, Type => TYPE, Instance => ID }
583
584A helper subroutine which creates a ticket group. (What RT 2.0 called Ticket watchers)
585Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner")
586Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
587Instance is the id of the ticket or queue in question
588
589This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
590
591Returns a tuple of (Id, Message). If id is 0, the create failed
592
593=cut
594
595sub CreateRoleGroup {
596 my $self = shift;
597 my %args = ( Instance => undef,
598 Type => undef,
599 Domain => undef,
600 @_ );
601
602 unless (RT::Queue->IsRoleGroupType($args{Type})) {
603 return ( 0, $self->loc("Invalid Group Type") );
604 }
605
606
607 return ( $self->_Create( Domain => $args{'Domain'},
608 Instance => $args{'Instance'},
609 Type => $args{'Type'},
610 InsideTransaction => 1 ) );
611}
612
613
614
615=head2 Delete
616
617Delete this object
618
619=cut
620
621sub Delete {
622 my $self = shift;
623
624 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
625 return ( 0, 'Permission Denied' );
626 }
627
628 $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
629 # TODO XXX
630
631 # Remove the principal object
632 # Remove this group from anything it's a member of.
633 # Remove all cached members of this group
634 # Remove any rights granted to this group
635 # remove any rights delegated by way of this group
636
637 return ( $self->SUPER::Delete(@_) );
638}
639
640
641=head2 SetDisabled BOOL
642
643If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
644It will not appear in most group listings.
645
646This routine finds all the cached group members that are members of this group (recursively) and disables them.
647
648=cut
649
650 # }}}
651
652 sub SetDisabled {
653 my $self = shift;
654 my $val = shift;
655 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
656 return (0, $self->loc('Permission Denied'));
657 }
658 $RT::Handle->BeginTransaction();
659 $self->PrincipalObj->SetDisabled($val);
660
661
662
663
664 # Find all occurrences of this member as a member of this group
665 # in the cache and nuke them, recursively.
666
667 # The following code will delete all Cached Group members
668 # where this member's group is _not_ the primary group
669 # (Ie if we're deleting C as a member of B, and B happens to be
670 # a member of A, will delete C as a member of A without touching
671 # C as a member of B
672
673 my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
674
675 $cached_submembers->Limit( FIELD => 'ImmediateParentId', OPERATOR => '=', VALUE => $self->Id);
676
677 #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space.
678 # TODO what about the groups key cache?
679 RT::Principal->InvalidateACLCache();
680
681
682
683 while ( my $item = $cached_submembers->Next() ) {
684 my $del_err = $item->SetDisabled($val);
685 unless ($del_err) {
686 $RT::Handle->Rollback();
687 $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
688 return (undef);
689 }
690 }
691
692 $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
693
694 $RT::Handle->Commit();
695 if ( $val == 1 ) {
696 return (1, $self->loc("Group disabled"));
697 } else {
698 return (1, $self->loc("Group enabled"));
699 }
700
701}
702
703
704
705
706sub Disabled {
707 my $self = shift;
708 $self->PrincipalObj->Disabled(@_);
709}
710
711
712
713=head2 DeepMembersObj
714
715Returns an RT::CachedGroupMembers object of this group's members,
716including all members of subgroups.
717
718=cut
719
720sub DeepMembersObj {
721 my $self = shift;
722 my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
723
724 #If we don't have rights, don't include any results
725 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
726 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
727
728 return ( $members_obj );
729
730}
731
732
733
734=head2 MembersObj
735
736Returns an RT::GroupMembers object of this group's direct members.
737
738=cut
739
740sub MembersObj {
741 my $self = shift;
742 my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
743
744 #If we don't have rights, don't include any results
745 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
746 $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
747
748 return ( $members_obj );
749
750}
751
752
753
754=head2 GroupMembersObj [Recursively => 1]
755
756Returns an L<RT::Groups> object of this group's members.
757By default returns groups including all subgroups, but
758could be changed with C<Recursively> named argument.
759
760B<Note> that groups are not filtered by type and result
761may contain as well system groups and others.
762
763=cut
764
765sub GroupMembersObj {
766 my $self = shift;
767 my %args = ( Recursively => 1, @_ );
768
769 my $groups = RT::Groups->new( $self->CurrentUser );
770 my $members_table = $args{'Recursively'}?
771 'CachedGroupMembers': 'GroupMembers';
772
773 my $members_alias = $groups->NewAlias( $members_table );
774 $groups->Join(
775 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
776 ALIAS2 => $groups->PrincipalsAlias, FIELD2 => 'id',
777 );
778 $groups->Limit(
779 ALIAS => $members_alias,
780 FIELD => 'GroupId',
781 VALUE => $self->PrincipalId,
782 );
783 $groups->Limit(
784 ALIAS => $members_alias,
785 FIELD => 'Disabled',
786 VALUE => 0,
787 ) if $args{'Recursively'};
788
789 return $groups;
790}
791
792
793
794=head2 UserMembersObj
795
796Returns an L<RT::Users> object of this group's members, by default
797returns users including all members of subgroups, but could be
798changed with C<Recursively> named argument.
799
800=cut
801
802sub UserMembersObj {
803 my $self = shift;
804 my %args = ( Recursively => 1, @_ );
805
806 #If we don't have rights, don't include any results
807 # TODO XXX WHY IS THERE NO ACL CHECK HERE?
808
809 my $members_table = $args{'Recursively'}?
810 'CachedGroupMembers': 'GroupMembers';
811
812 my $users = RT::Users->new($self->CurrentUser);
813 my $members_alias = $users->NewAlias( $members_table );
814 $users->Join(
815 ALIAS1 => $members_alias, FIELD1 => 'MemberId',
816 ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id',
817 );
818 $users->Limit(
819 ALIAS => $members_alias,
820 FIELD => 'GroupId',
821 VALUE => $self->PrincipalId,
822 );
823 $users->Limit(
824 ALIAS => $members_alias,
825 FIELD => 'Disabled',
826 VALUE => 0,
827 ) if $args{'Recursively'};
828
829 return ( $users);
830}
831
832
833
834=head2 MemberEmailAddresses
835
836Returns an array of the email addresses of all of this group's members
837
838
839=cut
840
841sub MemberEmailAddresses {
842 my $self = shift;
843 return sort grep defined && length,
844 map $_->EmailAddress,
845 @{ $self->UserMembersObj->ItemsArrayRef };
846}
847
848
849
850=head2 MemberEmailAddressesAsString
851
852Returns a comma delimited string of the email addresses of all users
853who are members of this group.
854
855=cut
856
857
858sub MemberEmailAddressesAsString {
859 my $self = shift;
860 return (join(', ', $self->MemberEmailAddresses));
861}
862
863
864
865=head2 AddMember PRINCIPAL_ID
866
867AddMember adds a principal to this group. It takes a single principal id.
868Returns a two value array. the first value is true on successful
869addition or 0 on failure. The second value is a textual status msg.
870
871=cut
872
873sub AddMember {
874 my $self = shift;
875 my $new_member = shift;
876
877
878
879 # We should only allow membership changes if the user has the right
880 # to modify group membership or the user is the principal in question
881 # and the user has the right to modify his own membership
882 unless ( ($new_member == $self->CurrentUser->PrincipalId &&
883 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
884 $self->CurrentUserHasRight('AdminGroupMembership') ) {
885 #User has no permission to be doing this
886 return ( 0, $self->loc("Permission Denied") );
887 }
888
889 $self->_AddMember(PrincipalId => $new_member);
890}
891
892# A helper subroutine for AddMember that bypasses the ACL checks
893# this should _ONLY_ ever be called from Ticket/Queue AddWatcher
894# when we want to deal with groups according to queue rights
895# In the dim future, this will all get factored out and life
896# will get better
897
898# takes a paramhash of { PrincipalId => undef, InsideTransaction }
899
900sub _AddMember {
901 my $self = shift;
902 my %args = ( PrincipalId => undef,
903 InsideTransaction => undef,
904 @_);
905 my $new_member = $args{'PrincipalId'};
906
907 unless ($self->Id) {
908 $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
909 return(0, $self->loc("Group not found"));
910 }
911
912 unless ($new_member =~ /^\d+$/) {
913 $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
914 }
915
916
917 my $new_member_obj = RT::Principal->new( $self->CurrentUser );
918 $new_member_obj->Load($new_member);
919
920
921 unless ( $new_member_obj->Id ) {
922 $RT::Logger->debug("Couldn't find that principal");
923 return ( 0, $self->loc("Couldn't find that principal") );
924 }
925
926 if ( $self->HasMember( $new_member_obj ) ) {
927
928 #User is already a member of this group. no need to add it
929 return ( 0, $self->loc("Group already has member: [_1]", $new_member_obj->Object->Name) );
930 }
931 if ( $new_member_obj->IsGroup &&
932 $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
933
934 #This group can't be made to be a member of itself
935 return ( 0, $self->loc("Groups can't be members of their members"));
936 }
937
938
939 my $member_object = RT::GroupMember->new( $self->CurrentUser );
940 my $id = $member_object->Create(
941 Member => $new_member_obj,
942 Group => $self->PrincipalObj,
943 InsideTransaction => $args{'InsideTransaction'}
944 );
945 if ($id) {
946 return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
947 }
948 else {
949 return(0, $self->loc("Couldn't add member to group"));
950 }
951}
952
953
954=head2 HasMember RT::Principal|id
955
956Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a
957member of this group.
958Returns undef if the user isn't a member of the group or if the current
959user doesn't have permission to find out. Arguably, it should differentiate
960between ACL failure and non membership.
961
962=cut
963
964sub HasMember {
965 my $self = shift;
966 my $principal = shift;
967
968 my $id;
969 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
970 $id = $principal->id;
971 } elsif ( $principal =~ /^\d+$/ ) {
972 $id = $principal;
973 } else {
974 $RT::Logger->error("Group::HasMember was called with an argument that".
975 " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
976 return(undef);
977 }
978 return undef unless $id;
979
980 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
981 $member_obj->LoadByCols(
982 MemberId => $id,
983 GroupId => $self->PrincipalId
984 );
985
986 if ( my $member_id = $member_obj->id ) {
987 return $member_id;
988 }
989 else {
990 return (undef);
991 }
992}
993
994
995
996=head2 HasMemberRecursively RT::Principal|id
997
998Takes an L<RT::Principal> object or its id and returns true if that user is a member of
999this group.
1000Returns undef if the user isn't a member of the group or if the current
1001user doesn't have permission to find out. Arguably, it should differentiate
1002between ACL failure and non membership.
1003
1004=cut
1005
1006sub HasMemberRecursively {
1007 my $self = shift;
1008 my $principal = shift;
1009
1010 my $id;
1011 if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1012 $id = $principal->id;
1013 } elsif ( $principal =~ /^\d+$/ ) {
1014 $id = $principal;
1015 } else {
1016 $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1017 " isn't an RT::Principal or id. It's $principal");
1018 return(undef);
1019 }
1020 return undef unless $id;
1021
1022 my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1023 $member_obj->LoadByCols(
1024 MemberId => $id,
1025 GroupId => $self->PrincipalId
1026 );
1027
1028 if ( my $member_id = $member_obj->id ) {
1029 return $member_id;
1030 }
1031 else {
1032 return (undef);
1033 }
1034}
1035
1036
1037
1038=head2 DeleteMember PRINCIPAL_ID
1039
1040Takes the principal id of a current user or group.
1041If the current user has apropriate rights,
1042removes that GroupMember from this group.
1043Returns a two value array. the first value is true on successful
1044addition or 0 on failure. The second value is a textual status msg.
1045
1046=cut
1047
1048sub DeleteMember {
1049 my $self = shift;
1050 my $member_id = shift;
1051
1052
1053 # We should only allow membership changes if the user has the right
1054 # to modify group membership or the user is the principal in question
1055 # and the user has the right to modify his own membership
1056
1057 unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1058 $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1059 $self->CurrentUserHasRight('AdminGroupMembership') ) {
1060 #User has no permission to be doing this
1061 return ( 0, $self->loc("Permission Denied") );
1062 }
1063 $self->_DeleteMember($member_id);
1064}
1065
1066# A helper subroutine for DeleteMember that bypasses the ACL checks
1067# this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
1068# when we want to deal with groups according to queue rights
1069# In the dim future, this will all get factored out and life
1070# will get better
1071
1072sub _DeleteMember {
1073 my $self = shift;
1074 my $member_id = shift;
1075
1076 my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1077
1078 $member_obj->LoadByCols( MemberId => $member_id,
1079 GroupId => $self->PrincipalId);
1080
1081
1082 #If we couldn't load it, return undef.
1083 unless ( $member_obj->Id() ) {
1084 $RT::Logger->debug("Group has no member with that id");
1085 return ( 0,$self->loc( "Group has no such member" ));
1086 }
1087
1088 #Now that we've checked ACLs and sanity, delete the groupmember
1089 my $val = $member_obj->Delete();
1090
1091 if ($val) {
1092 return ( $val, $self->loc("Member deleted") );
1093 }
1094 else {
1095 $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1096 return ( 0, $self->loc("Member not deleted" ));
1097 }
1098}
1099
1100
1101
1102sub _Set {
1103 my $self = shift;
1104 my %args = (
1105 Field => undef,
1106 Value => undef,
1107 TransactionType => 'Set',
1108 RecordTransaction => 1,
1109 @_
1110 );
1111
1112 unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1113 return ( 0, $self->loc('Permission Denied') );
1114 }
1115
1116 my $Old = $self->SUPER::_Value("$args{'Field'}");
1117
1118 my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1119 Value => $args{'Value'} );
1120
1121 #If we can't actually set the field to the value, don't record
1122 # a transaction. instead, get out of here.
1123 if ( $ret == 0 ) { return ( 0, $msg ); }
1124
1125 if ( $args{'RecordTransaction'} == 1 ) {
1126
1127 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1128 Type => $args{'TransactionType'},
1129 Field => $args{'Field'},
1130 NewValue => $args{'Value'},
1131 OldValue => $Old,
1132 TimeTaken => $args{'TimeTaken'},
1133 );
1134 return ( $Trans, scalar $TransObj->Description );
1135 }
1136 else {
1137 return ( $ret, $msg );
1138 }
1139}
1140
1141
1142
1143
1144
1145=head2 CurrentUserHasRight RIGHTNAME
1146
1147Returns true if the current user has the specified right for this group.
1148
1149
1150 TODO: we don't deal with membership visibility yet
1151
1152=cut
1153
1154
1155sub CurrentUserHasRight {
1156 my $self = shift;
1157 my $right = shift;
1158
1159
1160
1161 if ($self->Id &&
1162 $self->CurrentUser->HasRight( Object => $self,
1163 Right => $right )) {
1164 return(1);
1165 }
1166 elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right => $right )) {
1167 return (1);
1168 } else {
1169 return(undef);
1170 }
1171
1172}
1173
1174
1175=head2 CurrentUserCanSee
1176
1177Always returns 1; unfortunately, for historical reasons, users have
1178always been able to examine groups they have indirect access to, even if
1179they do not have SeeGroup explicitly.
1180
1181=cut
1182
1183sub CurrentUserCanSee {
1184 my $self = shift;
1185 return 1;
1186}
1187
1188
1189=head2 PrincipalObj
1190
1191Returns the principal object for this user. returns an empty RT::Principal
1192if there's no principal object matching this user.
1193The response is cached. PrincipalObj should never ever change.
1194
1195
1196=cut
1197
1198
1199sub PrincipalObj {
1200 my $self = shift;
1201 unless ( defined $self->{'PrincipalObj'} &&
1202 defined $self->{'PrincipalObj'}->ObjectId &&
1203 ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
1204 (defined $self->{'PrincipalObj'}->PrincipalType &&
1205 $self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
1206
1207 $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
1208 $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
1209 'PrincipalType' => 'Group') ;
1210 }
1211 return($self->{'PrincipalObj'});
1212}
1213
1214
1215=head2 PrincipalId
1216
1217Returns this user's PrincipalId
1218
1219=cut
1220
1221sub PrincipalId {
1222 my $self = shift;
1223 return $self->Id;
1224}
1225
1226
1227sub BasicColumns {
1228 (
1229 [ Name => 'Name' ],
1230 [ Description => 'Description' ],
1231 );
1232}
1233
1234
1235=head1 AUTHOR
1236
1237Jesse Vincent, jesse@bestpractical.com
1238
1239=head1 SEE ALSO
1240
1241RT
1242
1243=cut
1244
1245
1246
1247
1248
1249=head2 id
1250
1251Returns the current value of id.
1252(In the database, id is stored as int(11).)
1253
1254
1255=cut
1256
1257
1258=head2 Name
1259
1260Returns the current value of Name.
1261(In the database, Name is stored as varchar(200).)
1262
1263
1264
1265=head2 SetName VALUE
1266
1267
1268Set Name to VALUE.
1269Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1270(In the database, Name will be stored as a varchar(200).)
1271
1272
1273=cut
1274
1275
1276=head2 Description
1277
1278Returns the current value of Description.
1279(In the database, Description is stored as varchar(255).)
1280
1281
1282
1283=head2 SetDescription VALUE
1284
1285
1286Set Description to VALUE.
1287Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1288(In the database, Description will be stored as a varchar(255).)
1289
1290
1291=cut
1292
1293
1294=head2 Domain
1295
1296Returns the current value of Domain.
1297(In the database, Domain is stored as varchar(64).)
1298
1299
1300
1301=head2 SetDomain VALUE
1302
1303
1304Set Domain to VALUE.
1305Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1306(In the database, Domain will be stored as a varchar(64).)
1307
1308
1309=cut
1310
1311
1312=head2 Type
1313
1314Returns the current value of Type.
1315(In the database, Type is stored as varchar(64).)
1316
1317
1318
1319=head2 SetType VALUE
1320
1321
1322Set Type to VALUE.
1323Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1324(In the database, Type will be stored as a varchar(64).)
1325
1326
1327=cut
1328
1329
1330=head2 Instance
1331
1332Returns the current value of Instance.
1333(In the database, Instance is stored as int(11).)
1334
1335
1336
1337=head2 SetInstance VALUE
1338
1339
1340Set Instance to VALUE.
1341Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1342(In the database, Instance will be stored as a int(11).)
1343
1344
1345=cut
1346
1347
1348=head2 Creator
1349
1350Returns the current value of Creator.
1351(In the database, Creator is stored as int(11).)
1352
1353
1354=cut
1355
1356
1357=head2 Created
1358
1359Returns the current value of Created.
1360(In the database, Created is stored as datetime.)
1361
1362
1363=cut
1364
1365
1366=head2 LastUpdatedBy
1367
1368Returns the current value of LastUpdatedBy.
1369(In the database, LastUpdatedBy is stored as int(11).)
1370
1371
1372=cut
1373
1374
1375=head2 LastUpdated
1376
1377Returns the current value of LastUpdated.
1378(In the database, LastUpdated is stored as datetime.)
1379
1380
1381=cut
1382
1383
1384
1385sub _CoreAccessible {
1386 {
1387
1388 id =>
1389 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1390 Name =>
1391 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
1392 Description =>
1393 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1394 Domain =>
1395 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1396 Type =>
1397 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1398 Instance =>
1399 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1400 Creator =>
1401 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1402 Created =>
1403 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1404 LastUpdatedBy =>
1405 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1406 LastUpdated =>
1407 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1408
1409 }
1410};
1411
1412RT::Base->_ImportOverlays();
1413
14141;