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