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