b367b2f967cd0b4c84883a181db4c25b62a0255b
[usit-rt.git] / lib / RT / Group.pm
1
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2012 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     return (0, $self->loc("Group name '[_1]' is already in use", $value))
533         if $dupcheck->id;
534     return 1;
535 }
536
537 =head2 _CreateACLEquivalenceGroup { Principal }
538
539 A helper subroutine which creates a group containing only 
540 an individual user. This gets used by the ACL system to check rights.
541 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
542
543 Returns a tuple of (Id, Message).  If id is 0, the create failed
544
545 =cut
546
547 sub _CreateACLEquivalenceGroup { 
548     my $self = shift;
549     my $princ = shift;
550  
551       my $id = $self->_Create( Domain => 'ACLEquivalence', 
552                            Type => 'UserEquiv',
553                            Name => 'User '. $princ->Object->Id,
554                            Description => 'ACL equiv. for user '.$princ->Object->Id,
555                            Instance => $princ->Id,
556                            InsideTransaction => 1,
557                            _RecordTransaction => 0 );
558       unless ($id) {
559         $RT::Logger->crit("Couldn't create ACL equivalence group");
560         return undef;
561       }
562     
563        # We use stashuser so we don't get transactions inside transactions
564        # and so we bypass all sorts of cruft we don't need
565        my $aclstash = RT::GroupMember->new($self->CurrentUser);
566        my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
567                                              Member => $princ);
568
569       unless ($stash_id) {
570         $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
571         # We call super delete so we don't get acl checked.
572         $self->SUPER::Delete();
573         return(undef);
574       }
575     return ($id);
576 }
577
578
579
580
581 =head2 CreateRoleGroup { Domain => DOMAIN, Type =>  TYPE, Instance => ID }
582
583 A helper subroutine which creates a  ticket group. (What RT 2.0 called Ticket watchers)
584 Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner") 
585 Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
586 Instance is the id of the ticket or queue in question
587
588 This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
589
590 Returns a tuple of (Id, Message).  If id is 0, the create failed
591
592 =cut
593
594 sub CreateRoleGroup {
595     my $self = shift;
596     my %args = ( Instance => undef,
597                  Type     => undef,
598                  Domain   => undef,
599                  @_ );
600
601     unless (RT::Queue->IsRoleGroupType($args{Type})) {
602         return ( 0, $self->loc("Invalid Group Type") );
603     }
604
605
606     return ( $self->_Create( Domain            => $args{'Domain'},
607                              Instance          => $args{'Instance'},
608                              Type              => $args{'Type'},
609                              InsideTransaction => 1 ) );
610 }
611
612
613
614 =head2 Delete
615
616 Delete this object
617
618 =cut
619
620 sub Delete {
621     my $self = shift;
622
623     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
624         return ( 0, 'Permission Denied' );
625     }
626
627     $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
628     # TODO XXX 
629    
630     # Remove the principal object
631     # Remove this group from anything it's a member of.
632     # Remove all cached members of this group
633     # Remove any rights granted to this group
634     # remove any rights delegated by way of this group
635
636     return ( $self->SUPER::Delete(@_) );
637 }
638
639
640 =head2 SetDisabled BOOL
641
642 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
643 It will not appear in most group listings.
644
645 This routine finds all the cached group members that are members of this group  (recursively) and disables them.
646
647 =cut 
648
649  # }}}
650
651  sub SetDisabled {
652      my $self = shift;
653      my $val = shift;
654      unless ( $self->CurrentUserHasRight('AdminGroup') ) {
655         return (0, $self->loc('Permission Denied'));
656     }
657     $RT::Handle->BeginTransaction();
658     $self->PrincipalObj->SetDisabled($val);
659
660
661
662
663     # Find all occurrences of this member as a member of this group
664     # in the cache and nuke them, recursively.
665
666     # The following code will delete all Cached Group members
667     # where this member's group is _not_ the primary group 
668     # (Ie if we're deleting C as a member of B, and B happens to be 
669     # a member of A, will delete C as a member of A without touching
670     # C as a member of B
671
672     my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
673
674     $cached_submembers->Limit( FIELD    => 'ImmediateParentId', OPERATOR => '=', VALUE    => $self->Id);
675
676     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
677     # TODO what about the groups key cache?
678     RT::Principal->InvalidateACLCache();
679
680
681
682     while ( my $item = $cached_submembers->Next() ) {
683         my $del_err = $item->SetDisabled($val);
684         unless ($del_err) {
685             $RT::Handle->Rollback();
686             $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
687             return (undef);
688         }
689     }
690
691     $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
692
693     $RT::Handle->Commit();
694     if ( $val == 1 ) {
695         return (1, $self->loc("Group disabled"));
696     } else {
697         return (1, $self->loc("Group enabled"));
698     }
699
700 }
701
702
703
704
705 sub Disabled {
706     my $self = shift;
707     $self->PrincipalObj->Disabled(@_);
708 }
709
710
711
712 =head2 DeepMembersObj
713
714 Returns an RT::CachedGroupMembers object of this group's members,
715 including all members of subgroups.
716
717 =cut
718
719 sub DeepMembersObj {
720     my $self = shift;
721     my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
722
723     #If we don't have rights, don't include any results
724     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
725     $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
726
727     return ( $members_obj );
728
729 }
730
731
732
733 =head2 MembersObj
734
735 Returns an RT::GroupMembers object of this group's direct members.
736
737 =cut
738
739 sub MembersObj {
740     my $self = shift;
741     my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
742
743     #If we don't have rights, don't include any results
744     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
745     $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
746
747     return ( $members_obj );
748
749 }
750
751
752
753 =head2 GroupMembersObj [Recursively => 1]
754
755 Returns an L<RT::Groups> object of this group's members.
756 By default returns groups including all subgroups, but
757 could be changed with C<Recursively> named argument.
758
759 B<Note> that groups are not filtered by type and result
760 may contain as well system groups and others.
761
762 =cut
763
764 sub GroupMembersObj {
765     my $self = shift;
766     my %args = ( Recursively => 1, @_ );
767
768     my $groups = RT::Groups->new( $self->CurrentUser );
769     my $members_table = $args{'Recursively'}?
770         'CachedGroupMembers': 'GroupMembers';
771
772     my $members_alias = $groups->NewAlias( $members_table );
773     $groups->Join(
774         ALIAS1 => $members_alias,           FIELD1 => 'MemberId',
775         ALIAS2 => $groups->PrincipalsAlias, FIELD2 => 'id',
776     );
777     $groups->Limit(
778         ALIAS    => $members_alias,
779         FIELD    => 'GroupId',
780         VALUE    => $self->PrincipalId,
781     );
782     $groups->Limit(
783         ALIAS => $members_alias,
784         FIELD => 'Disabled',
785         VALUE => 0,
786     ) if $args{'Recursively'};
787
788     return $groups;
789 }
790
791
792
793 =head2 UserMembersObj
794
795 Returns an L<RT::Users> object of this group's members, by default
796 returns users including all members of subgroups, but could be
797 changed with C<Recursively> named argument.
798
799 =cut
800
801 sub UserMembersObj {
802     my $self = shift;
803     my %args = ( Recursively => 1, @_ );
804
805     #If we don't have rights, don't include any results
806     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
807
808     my $members_table = $args{'Recursively'}?
809         'CachedGroupMembers': 'GroupMembers';
810
811     my $users = RT::Users->new($self->CurrentUser);
812     my $members_alias = $users->NewAlias( $members_table );
813     $users->Join(
814         ALIAS1 => $members_alias,           FIELD1 => 'MemberId',
815         ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id',
816     );
817     $users->Limit(
818         ALIAS => $members_alias,
819         FIELD => 'GroupId',
820         VALUE => $self->PrincipalId,
821     );
822     $users->Limit(
823         ALIAS => $members_alias,
824         FIELD => 'Disabled',
825         VALUE => 0,
826     ) if $args{'Recursively'};
827
828     return ( $users);
829 }
830
831
832
833 =head2 MemberEmailAddresses
834
835 Returns an array of the email addresses of all of this group's members
836
837
838 =cut
839
840 sub MemberEmailAddresses {
841     my $self = shift;
842     return sort grep defined && length,
843         map $_->EmailAddress,
844         @{ $self->UserMembersObj->ItemsArrayRef };
845 }
846
847
848
849 =head2 MemberEmailAddressesAsString
850
851 Returns a comma delimited string of the email addresses of all users 
852 who are members of this group.
853
854 =cut
855
856
857 sub MemberEmailAddressesAsString {
858     my $self = shift;
859     return (join(', ', $self->MemberEmailAddresses));
860 }
861
862
863
864 =head2 AddMember PRINCIPAL_ID
865
866 AddMember adds a principal to this group.  It takes a single principal id.
867 Returns a two value array. the first value is true on successful 
868 addition or 0 on failure.  The second value is a textual status msg.
869
870 =cut
871
872 sub AddMember {
873     my $self       = shift;
874     my $new_member = shift;
875
876
877
878     # We should only allow membership changes if the user has the right 
879     # to modify group membership or the user is the principal in question
880     # and the user has the right to modify his own membership
881     unless ( ($new_member == $self->CurrentUser->PrincipalId &&
882               $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
883               $self->CurrentUserHasRight('AdminGroupMembership') ) {
884         #User has no permission to be doing this
885         return ( 0, $self->loc("Permission Denied") );
886     }
887
888     $self->_AddMember(PrincipalId => $new_member);
889 }
890
891 # A helper subroutine for AddMember that bypasses the ACL checks
892 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
893 # when we want to deal with groups according to queue rights
894 # In the dim future, this will all get factored out and life
895 # will get better       
896
897 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
898
899 sub _AddMember {
900     my $self = shift;
901     my %args = ( PrincipalId => undef,
902                  InsideTransaction => undef,
903                  @_);
904     my $new_member = $args{'PrincipalId'};
905
906     unless ($self->Id) {
907         $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
908         return(0, $self->loc("Group not found"));
909     }
910
911     unless ($new_member =~ /^\d+$/) {
912         $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
913     }
914
915
916     my $new_member_obj = RT::Principal->new( $self->CurrentUser );
917     $new_member_obj->Load($new_member);
918
919
920     unless ( $new_member_obj->Id ) {
921         $RT::Logger->debug("Couldn't find that principal");
922         return ( 0, $self->loc("Couldn't find that principal") );
923     }
924
925     if ( $self->HasMember( $new_member_obj ) ) {
926
927         #User is already a member of this group. no need to add it
928         return ( 0, $self->loc("Group already has member: [_1]", $new_member_obj->Object->Name) );
929     }
930     if ( $new_member_obj->IsGroup &&
931          $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
932
933         #This group can't be made to be a member of itself
934         return ( 0, $self->loc("Groups can't be members of their members"));
935     }
936
937
938     my $member_object = RT::GroupMember->new( $self->CurrentUser );
939     my $id = $member_object->Create(
940         Member => $new_member_obj,
941         Group => $self->PrincipalObj,
942         InsideTransaction => $args{'InsideTransaction'}
943     );
944     if ($id) {
945         return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
946     }
947     else {
948         return(0, $self->loc("Couldn't add member to group"));
949     }
950 }
951
952
953 =head2 HasMember RT::Principal|id
954
955 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a 
956 member of this group.
957 Returns undef if the user isn't a member of the group or if the current
958 user doesn't have permission to find out. Arguably, it should differentiate
959 between ACL failure and non membership.
960
961 =cut
962
963 sub HasMember {
964     my $self    = shift;
965     my $principal = shift;
966
967     my $id;
968     if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
969         $id = $principal->id;
970     } elsif ( $principal =~ /^\d+$/ ) {
971         $id = $principal;
972     } else {
973         $RT::Logger->error("Group::HasMember was called with an argument that".
974                           " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
975         return(undef);
976     }
977     return undef unless $id;
978
979     my $member_obj = RT::GroupMember->new( $self->CurrentUser );
980     $member_obj->LoadByCols(
981         MemberId => $id, 
982         GroupId  => $self->PrincipalId
983     );
984
985     if ( my $member_id = $member_obj->id ) {
986         return $member_id;
987     }
988     else {
989         return (undef);
990     }
991 }
992
993
994
995 =head2 HasMemberRecursively RT::Principal|id
996
997 Takes an L<RT::Principal> object or its id and returns true if that user is a member of 
998 this group.
999 Returns undef if the user isn't a member of the group or if the current
1000 user doesn't have permission to find out. Arguably, it should differentiate
1001 between ACL failure and non membership.
1002
1003 =cut
1004
1005 sub HasMemberRecursively {
1006     my $self    = shift;
1007     my $principal = shift;
1008
1009     my $id;
1010     if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1011         $id = $principal->id;
1012     } elsif ( $principal =~ /^\d+$/ ) {
1013         $id = $principal;
1014     } else {
1015         $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1016                           " isn't an RT::Principal or id. It's $principal");
1017         return(undef);
1018     }
1019     return undef unless $id;
1020
1021     my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1022     $member_obj->LoadByCols(
1023         MemberId => $id, 
1024         GroupId  => $self->PrincipalId
1025     );
1026
1027     if ( my $member_id = $member_obj->id ) {
1028         return $member_id;
1029     }
1030     else {
1031         return (undef);
1032     }
1033 }
1034
1035
1036
1037 =head2 DeleteMember PRINCIPAL_ID
1038
1039 Takes the principal id of a current user or group.
1040 If the current user has apropriate rights,
1041 removes that GroupMember from this group.
1042 Returns a two value array. the first value is true on successful 
1043 addition or 0 on failure.  The second value is a textual status msg.
1044
1045 =cut
1046
1047 sub DeleteMember {
1048     my $self   = shift;
1049     my $member_id = shift;
1050
1051
1052     # We should only allow membership changes if the user has the right 
1053     # to modify group membership or the user is the principal in question
1054     # and the user has the right to modify his own membership
1055
1056     unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1057               $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1058               $self->CurrentUserHasRight('AdminGroupMembership') ) {
1059         #User has no permission to be doing this
1060         return ( 0, $self->loc("Permission Denied") );
1061     }
1062     $self->_DeleteMember($member_id);
1063 }
1064
1065 # A helper subroutine for DeleteMember that bypasses the ACL checks
1066 # this should _ONLY_ ever be called from Ticket/Queue  DeleteWatcher
1067 # when we want to deal with groups according to queue rights
1068 # In the dim future, this will all get factored out and life
1069 # will get better       
1070
1071 sub _DeleteMember {
1072     my $self = shift;
1073     my $member_id = shift;
1074
1075     my $member_obj =  RT::GroupMember->new( $self->CurrentUser );
1076     
1077     $member_obj->LoadByCols( MemberId  => $member_id,
1078                              GroupId => $self->PrincipalId);
1079
1080
1081     #If we couldn't load it, return undef.
1082     unless ( $member_obj->Id() ) {
1083         $RT::Logger->debug("Group has no member with that id");
1084         return ( 0,$self->loc( "Group has no such member" ));
1085     }
1086
1087     #Now that we've checked ACLs and sanity, delete the groupmember
1088     my $val = $member_obj->Delete();
1089
1090     if ($val) {
1091         return ( $val, $self->loc("Member deleted") );
1092     }
1093     else {
1094         $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1095         return ( 0, $self->loc("Member not deleted" ));
1096     }
1097 }
1098
1099
1100
1101 sub _Set {
1102     my $self = shift;
1103     my %args = (
1104         Field => undef,
1105         Value => undef,
1106         TransactionType   => 'Set',
1107         RecordTransaction => 1,
1108         @_
1109     );
1110
1111     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1112         return ( 0, $self->loc('Permission Denied') );
1113         }
1114
1115     my $Old = $self->SUPER::_Value("$args{'Field'}");
1116     
1117     my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1118                                           Value => $args{'Value'} );
1119     
1120     #If we can't actually set the field to the value, don't record
1121     # a transaction. instead, get out of here.
1122     if ( $ret == 0 ) { return ( 0, $msg ); }
1123
1124     if ( $args{'RecordTransaction'} == 1 ) {
1125
1126         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1127                                                Type => $args{'TransactionType'},
1128                                                Field     => $args{'Field'},
1129                                                NewValue  => $args{'Value'},
1130                                                OldValue  => $Old,
1131                                                TimeTaken => $args{'TimeTaken'},
1132         );
1133         return ( $Trans, scalar $TransObj->Description );
1134     }
1135     else {
1136         return ( $ret, $msg );
1137     }
1138 }
1139
1140
1141
1142
1143
1144 =head2 CurrentUserHasRight RIGHTNAME
1145
1146 Returns true if the current user has the specified right for this group.
1147
1148
1149     TODO: we don't deal with membership visibility yet
1150
1151 =cut
1152
1153
1154 sub CurrentUserHasRight {
1155     my $self = shift;
1156     my $right = shift;
1157
1158
1159
1160     if ($self->Id && 
1161                 $self->CurrentUser->HasRight( Object => $self,
1162                                                                                    Right => $right )) {
1163         return(1);
1164    }
1165     elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right =>  $right )) {
1166                 return (1);
1167     } else {
1168         return(undef);
1169     }
1170
1171 }
1172
1173
1174 =head2 CurrentUserCanSee
1175
1176 Always returns 1; unfortunately, for historical reasons, users have
1177 always been able to examine groups they have indirect access to, even if
1178 they do not have SeeGroup explicitly.
1179
1180 =cut
1181
1182 sub CurrentUserCanSee {
1183     my $self = shift;
1184     return 1;
1185 }
1186
1187
1188 =head2 PrincipalObj
1189
1190 Returns the principal object for this user. returns an empty RT::Principal
1191 if there's no principal object matching this user. 
1192 The response is cached. PrincipalObj should never ever change.
1193
1194
1195 =cut
1196
1197
1198 sub PrincipalObj {
1199     my $self = shift;
1200     unless ( defined $self->{'PrincipalObj'} &&
1201              defined $self->{'PrincipalObj'}->ObjectId &&
1202             ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
1203             (defined $self->{'PrincipalObj'}->PrincipalType && 
1204                 $self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
1205
1206             $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
1207             $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
1208                                                 'PrincipalType' => 'Group') ;
1209             }
1210     return($self->{'PrincipalObj'});
1211 }
1212
1213
1214 =head2 PrincipalId  
1215
1216 Returns this user's PrincipalId
1217
1218 =cut
1219
1220 sub PrincipalId {
1221     my $self = shift;
1222     return $self->Id;
1223 }
1224
1225
1226 sub BasicColumns {
1227     (
1228         [ Name => 'Name' ],
1229         [ Description => 'Description' ],
1230     );
1231 }
1232
1233
1234 =head1 AUTHOR
1235
1236 Jesse Vincent, jesse@bestpractical.com
1237
1238 =head1 SEE ALSO
1239
1240 RT
1241
1242 =cut
1243
1244
1245
1246
1247
1248 =head2 id
1249
1250 Returns the current value of id.
1251 (In the database, id is stored as int(11).)
1252
1253
1254 =cut
1255
1256
1257 =head2 Name
1258
1259 Returns the current value of Name.
1260 (In the database, Name is stored as varchar(200).)
1261
1262
1263
1264 =head2 SetName VALUE
1265
1266
1267 Set Name to VALUE.
1268 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1269 (In the database, Name will be stored as a varchar(200).)
1270
1271
1272 =cut
1273
1274
1275 =head2 Description
1276
1277 Returns the current value of Description.
1278 (In the database, Description is stored as varchar(255).)
1279
1280
1281
1282 =head2 SetDescription VALUE
1283
1284
1285 Set Description to VALUE.
1286 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1287 (In the database, Description will be stored as a varchar(255).)
1288
1289
1290 =cut
1291
1292
1293 =head2 Domain
1294
1295 Returns the current value of Domain.
1296 (In the database, Domain is stored as varchar(64).)
1297
1298
1299
1300 =head2 SetDomain VALUE
1301
1302
1303 Set Domain to VALUE.
1304 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1305 (In the database, Domain will be stored as a varchar(64).)
1306
1307
1308 =cut
1309
1310
1311 =head2 Type
1312
1313 Returns the current value of Type.
1314 (In the database, Type is stored as varchar(64).)
1315
1316
1317
1318 =head2 SetType VALUE
1319
1320
1321 Set Type to VALUE.
1322 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1323 (In the database, Type will be stored as a varchar(64).)
1324
1325
1326 =cut
1327
1328
1329 =head2 Instance
1330
1331 Returns the current value of Instance.
1332 (In the database, Instance is stored as int(11).)
1333
1334
1335
1336 =head2 SetInstance VALUE
1337
1338
1339 Set Instance to VALUE.
1340 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1341 (In the database, Instance will be stored as a int(11).)
1342
1343
1344 =cut
1345
1346
1347 =head2 Creator
1348
1349 Returns the current value of Creator.
1350 (In the database, Creator is stored as int(11).)
1351
1352
1353 =cut
1354
1355
1356 =head2 Created
1357
1358 Returns the current value of Created.
1359 (In the database, Created is stored as datetime.)
1360
1361
1362 =cut
1363
1364
1365 =head2 LastUpdatedBy
1366
1367 Returns the current value of LastUpdatedBy.
1368 (In the database, LastUpdatedBy is stored as int(11).)
1369
1370
1371 =cut
1372
1373
1374 =head2 LastUpdated
1375
1376 Returns the current value of LastUpdated.
1377 (In the database, LastUpdated is stored as datetime.)
1378
1379
1380 =cut
1381
1382
1383
1384 sub _CoreAccessible {
1385     {
1386
1387         id =>
1388                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1389         Name =>
1390                 {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
1391         Description =>
1392                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1393         Domain =>
1394                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1395         Type =>
1396                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1397         Instance =>
1398                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1399         Creator =>
1400                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1401         Created =>
1402                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1403         LastUpdatedBy =>
1404                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1405         LastUpdated =>
1406                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1407
1408  }
1409 };
1410
1411 RT::Base->_ImportOverlays();
1412
1413 1;