rt 4.0.23
[freeside.git] / rt / lib / RT / Group.pm
1
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
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
54   RT::Group - RT's group object
55
56 =head1 SYNOPSIS
57
58 use RT::Group;
59 my $group = RT::Group->new($CurrentUser);
60
61 =head1 DESCRIPTION
62
63 An RT group object.
64
65 =head1 METHODS
66
67
68
69
70
71 =cut
72
73
74 package RT::Group;
75
76
77 use strict;
78 use warnings;
79
80 use base 'RT::Record';
81
82 sub Table {'Groups'}
83
84
85
86 use RT::Users;
87 use RT::GroupMembers;
88 use RT::Principals;
89 use RT::ACL;
90
91 use 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
133 Adds the given rights to the list of possible rights.  This method
134 should be called during server startup, not at runtime.
135
136 =cut
137
138 sub 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
148 Returns a hash of available rights for this object. The keys are the right names and the values are a description of what the rights do
149
150 =cut
151
152 sub AvailableRights {
153     my $self = shift;
154     return($RIGHTS);
155 }
156
157 =head2 RightCategories
158
159 Returns a hashref where the keys are rights for this type of object and the
160 values are the category (General, Staff, Admin) the right falls into.
161
162 =cut
163
164 sub RightCategories {
165     return $RIGHT_CATEGORIES;
166 }
167
168 =head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...]
169
170 Adds the given right and category pairs to the list of right categories.  This
171 method should be called during server startup, not at runtime.
172
173 =cut
174
175 sub 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
185 Returns a user-readable description of what this group is for and what it's named.
186
187 =cut
188
189 sub 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
222 Load a group object from the database. Takes a single argument.
223 If the argument is numerical, load by the column 'id'. Otherwise, 
224 complain and return.
225
226 =cut
227
228 sub 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
245 Loads a system group from the database. The only argument is
246 the group's name.
247
248
249 =cut
250
251 sub 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
272 Loads a user's acl equivalence group. Takes a principal object or its ID.
273 ACL equivalnce groups are used to simplify the acl system. Each user
274 has one group that only he is a member of. Rights granted to the user
275 are actually granted to that group. This greatly simplifies ACL checks.
276 While this results in a somewhat more complex setup when creating users
277 and granting ACLs, it _greatly_ simplifies acl checks.
278
279 =cut
280
281 sub 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
298 Loads a Pseudo group from the database. The only argument is
299 the group's name.
300
301
302 =cut
303
304 sub 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
318 Loads a ticket group from the database. 
319
320 Takes 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
328 sub 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
343 Loads a Queue group from the database. 
344
345 Takes 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
353 sub 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
368 Loads a System group from the database. 
369
370 Takes 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
377 sub 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
389 You need to specify what sort of group you're creating by calling one of the other
390 Create_____ routines.
391
392 =cut
393
394 sub 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
404 Takes a paramhash with named arguments: Name, Description.
405
406 Returns a tuple of (Id, Message).  If id is 0, the create failed
407
408 =cut
409
410 sub _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
484 A helper subroutine which creates a system group 
485
486 Returns a tuple of (Id, Message).  If id is 0, the create failed
487
488 =cut
489
490 sub 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
504 Enforces unique user defined group names when updating
505
506 =cut
507
508 sub 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
521 Returns true if the user defined group name isn't in use, false otherwise.
522
523 =cut
524
525 sub _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);
532     if ( $dupcheck->id && ( !$self->id || $self->id != $dupcheck->id ) ) {
533         return ( 0, $self->loc( "Group name '[_1]' is already in use", $value ) );
534     }
535     return 1;
536 }
537
538 =head2 _CreateACLEquivalenceGroup { Principal }
539
540 A helper subroutine which creates a group containing only 
541 an individual user. This gets used by the ACL system to check rights.
542 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
543
544 Returns a tuple of (Id, Message).  If id is 0, the create failed
545
546 =cut
547
548 sub _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
584 A helper subroutine which creates a  ticket group. (What RT 2.0 called Ticket watchers)
585 Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner") 
586 Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
587 Instance is the id of the ticket or queue in question
588
589 This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
590
591 Returns a tuple of (Id, Message).  If id is 0, the create failed
592
593 =cut
594
595 sub 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
617 Delete this object
618
619 =cut
620
621 sub 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
643 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
644 It will not appear in most group listings.
645
646 This 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
706 sub Disabled {
707     my $self = shift;
708     $self->PrincipalObj->Disabled(@_);
709 }
710
711
712
713 =head2 DeepMembersObj
714
715 Returns an RT::CachedGroupMembers object of this group's members,
716 including all members of subgroups.
717
718 =cut
719
720 sub 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
736 Returns an RT::GroupMembers object of this group's direct members.
737
738 =cut
739
740 sub 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
756 Returns an L<RT::Groups> object of this group's members.
757 By default returns groups including all subgroups, but
758 could be changed with C<Recursively> named argument.
759
760 B<Note> that groups are not filtered by type and result
761 may contain as well system groups and others.
762
763 =cut
764
765 sub 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
796 Returns an L<RT::Users> object of this group's members, by default
797 returns users including all members of subgroups, but could be
798 changed with C<Recursively> named argument.
799
800 =cut
801
802 sub 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
836 Returns an array of the email addresses of all of this group's members
837
838
839 =cut
840
841 sub 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
852 Returns a comma delimited string of the email addresses of all users 
853 who are members of this group.
854
855 =cut
856
857
858 sub MemberEmailAddressesAsString {
859     my $self = shift;
860     return (join(', ', $self->MemberEmailAddresses));
861 }
862
863
864
865 =head2 AddMember PRINCIPAL_ID
866
867 AddMember adds a principal to this group.  It takes a single principal id.
868 Returns a two value array. the first value is true on successful 
869 addition or 0 on failure.  The second value is a textual status msg.
870
871 =cut
872
873 sub 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
900 sub _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
956 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a 
957 member of this group.
958 Returns undef if the user isn't a member of the group or if the current
959 user doesn't have permission to find out. Arguably, it should differentiate
960 between ACL failure and non membership.
961
962 =cut
963
964 sub 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
998 Takes an L<RT::Principal> object or its id and returns true if that user is a member of 
999 this group.
1000 Returns undef if the user isn't a member of the group or if the current
1001 user doesn't have permission to find out. Arguably, it should differentiate
1002 between ACL failure and non membership.
1003
1004 =cut
1005
1006 sub 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
1040 Takes the principal id of a current user or group.
1041 If the current user has apropriate rights,
1042 removes that GroupMember from this group.
1043 Returns a two value array. the first value is true on successful 
1044 addition or 0 on failure.  The second value is a textual status msg.
1045
1046 =cut
1047
1048 sub 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
1072 sub _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
1102 sub _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
1147 Returns 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
1155 sub 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
1177 Always returns 1; unfortunately, for historical reasons, users have
1178 always been able to examine groups they have indirect access to, even if
1179 they do not have SeeGroup explicitly.
1180
1181 =cut
1182
1183 sub CurrentUserCanSee {
1184     my $self = shift;
1185     return 1;
1186 }
1187
1188
1189 =head2 PrincipalObj
1190
1191 Returns the principal object for this user. returns an empty RT::Principal
1192 if there's no principal object matching this user. 
1193 The response is cached. PrincipalObj should never ever change.
1194
1195
1196 =cut
1197
1198
1199 sub 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
1217 Returns this user's PrincipalId
1218
1219 =cut
1220
1221 sub PrincipalId {
1222     my $self = shift;
1223     return $self->Id;
1224 }
1225
1226
1227 sub BasicColumns {
1228     (
1229         [ Name => 'Name' ],
1230         [ Description => 'Description' ],
1231     );
1232 }
1233
1234
1235 =head1 AUTHOR
1236
1237 Jesse Vincent, jesse@bestpractical.com
1238
1239 =head1 SEE ALSO
1240
1241 RT
1242
1243 =cut
1244
1245
1246
1247
1248
1249 =head2 id
1250
1251 Returns 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
1260 Returns the current value of Name.
1261 (In the database, Name is stored as varchar(200).)
1262
1263
1264
1265 =head2 SetName VALUE
1266
1267
1268 Set Name to VALUE.
1269 Returns (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
1278 Returns the current value of Description.
1279 (In the database, Description is stored as varchar(255).)
1280
1281
1282
1283 =head2 SetDescription VALUE
1284
1285
1286 Set Description to VALUE.
1287 Returns (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
1296 Returns the current value of Domain.
1297 (In the database, Domain is stored as varchar(64).)
1298
1299
1300
1301 =head2 SetDomain VALUE
1302
1303
1304 Set Domain to VALUE.
1305 Returns (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
1314 Returns the current value of Type.
1315 (In the database, Type is stored as varchar(64).)
1316
1317
1318
1319 =head2 SetType VALUE
1320
1321
1322 Set Type to VALUE.
1323 Returns (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
1332 Returns the current value of Instance.
1333 (In the database, Instance is stored as int(11).)
1334
1335
1336
1337 =head2 SetInstance VALUE
1338
1339
1340 Set Instance to VALUE.
1341 Returns (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
1350 Returns 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
1359 Returns the current value of Created.
1360 (In the database, Created is stored as datetime.)
1361
1362
1363 =cut
1364
1365
1366 =head2 LastUpdatedBy
1367
1368 Returns 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
1377 Returns the current value of LastUpdated.
1378 (In the database, LastUpdated is stored as datetime.)
1379
1380
1381 =cut
1382
1383
1384
1385 sub _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
1412 RT::Base->_ImportOverlays();
1413
1414 1;