rt 4.2.15
[freeside.git] / rt / lib / RT / Group.pm
1
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2018 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 =cut
66
67
68 package RT::Group;
69
70
71 use strict;
72 use warnings;
73
74 use base 'RT::Record';
75
76 use Role::Basic 'with';
77 with "RT::Record::Role::Rights";
78
79 sub Table {'Groups'}
80
81
82
83 use RT::Users;
84 use RT::GroupMembers;
85 use RT::Principals;
86 use RT::ACL;
87
88 __PACKAGE__->AddRight( Admin => AdminGroup           => 'Modify group metadata or delete group'); # loc
89 __PACKAGE__->AddRight( Admin => AdminGroupMembership => 'Modify group membership roster'); # loc
90 __PACKAGE__->AddRight( Staff => ModifyOwnMembership  => 'Join or leave group'); # loc
91 __PACKAGE__->AddRight( Admin => EditSavedSearches    => 'Create, modify and delete saved searches'); # loc
92 __PACKAGE__->AddRight( Staff => ShowSavedSearches    => 'View saved searches'); # loc
93 __PACKAGE__->AddRight( Staff => SeeGroup             => 'View group'); # loc
94 __PACKAGE__->AddRight( Staff => SeeGroupDashboard    => 'View group dashboards'); # loc
95 __PACKAGE__->AddRight( Admin => CreateGroupDashboard => 'Create group dashboards'); # loc
96 __PACKAGE__->AddRight( Admin => ModifyGroupDashboard => 'Modify group dashboards'); # loc
97 __PACKAGE__->AddRight( Admin => DeleteGroupDashboard => 'Delete group dashboards'); # loc
98
99 =head1 METHODS
100
101 =head2 SelfDescription
102
103 Returns a user-readable description of what this group is for and what it's named.
104
105 =cut
106
107 sub SelfDescription {
108     my $self = shift;
109     if ($self->Domain eq 'ACLEquivalence') {
110         my $user = RT::Principal->new($self->CurrentUser);
111         $user->Load($self->Instance);
112         return $self->loc("user [_1]",$user->Object->Name);
113     }
114     elsif ($self->Domain eq 'UserDefined') {
115         return $self->loc("group '[_1]'",$self->Name);
116     }
117     elsif ($self->Domain eq 'RT::System-Role') {
118         return $self->loc("system [_1]",$self->Name);
119     }
120     elsif ($self->Domain eq 'RT::Queue-Role') {
121         my $queue = RT::Queue->new($self->CurrentUser);
122         $queue->Load($self->Instance);
123         return $self->loc("queue [_1] [_2]",$queue->Name, $self->Name);
124     }
125     elsif ($self->Domain eq 'RT::Ticket-Role') {
126         return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Name);
127     }
128     elsif ($self->RoleClass) {
129         my $class = lc $self->RoleClass;
130            $class =~ s/^RT:://i;
131         return $self->loc("[_1] #[_2] [_3]", $self->loc($class), $self->Instance, $self->Name);
132     }
133     elsif ($self->Domain eq 'SystemInternal') {
134         return $self->loc("system group '[_1]'",$self->Name);
135     }
136     else {
137         return $self->loc("undescribed group [_1]",$self->Id);
138     }
139 }
140
141
142
143 =head2 Load ID
144
145 Load a group object from the database. Takes a single argument.
146 If the argument is numerical, load by the column 'id'. Otherwise, 
147 complain and return.
148
149 =cut
150
151 sub Load {
152     my $self       = shift;
153     my $identifier = shift || return undef;
154
155     if ( $identifier !~ /\D/ ) {
156         $self->SUPER::LoadById($identifier);
157     }
158     else {
159         $RT::Logger->crit("Group -> Load called with a bogus argument");
160         return undef;
161     }
162 }
163
164
165
166 =head2 LoadUserDefinedGroup NAME
167
168 Loads a system group from the database. The only argument is
169 the group's name.
170
171
172 =cut
173
174 sub LoadUserDefinedGroup {
175     my $self       = shift;
176     my $identifier = shift;
177
178     if ( $identifier =~ /^\d+$/ ) {
179         return $self->LoadByCols(
180             Domain => 'UserDefined',
181             id     => $identifier,
182         );
183     } else {
184         return $self->LoadByCols(
185             Domain => 'UserDefined',
186             Name   => $identifier,
187         );
188     }
189 }
190
191
192
193 =head2 LoadACLEquivalenceGroup PRINCIPAL
194
195 Loads a user's acl equivalence group. Takes a principal object or its ID.
196 ACL equivalnce groups are used to simplify the acl system. Each user
197 has one group that only he is a member of. Rights granted to the user
198 are actually granted to that group. This greatly simplifies ACL checks.
199 While this results in a somewhat more complex setup when creating users
200 and granting ACLs, it _greatly_ simplifies acl checks.
201
202 =cut
203
204 sub LoadACLEquivalenceGroup {
205     my $self = shift;
206     my $principal = shift;
207     $principal = $principal->id if ref $principal;
208
209     return $self->LoadByCols(
210         Domain   => 'ACLEquivalence',
211         Name     => 'UserEquiv',
212         Instance => $principal,
213     );
214 }
215
216
217
218
219 =head2 LoadSystemInternalGroup NAME
220
221 Loads a Pseudo group from the database. The only argument is
222 the group's name.
223
224
225 =cut
226
227 sub LoadSystemInternalGroup {
228     my $self       = shift;
229     my $identifier = shift;
230
231     return $self->LoadByCols(
232         Domain => 'SystemInternal',
233         Name   => $identifier,
234     );
235 }
236
237 =head2 LoadRoleGroup
238
239 Takes a paramhash of Object and Name and attempts to load the suitable role
240 group for said object.
241
242 =cut
243
244 sub LoadRoleGroup {
245     my $self = shift;
246     my %args = (
247         Object  => undef,
248         Name    => undef,
249         @_
250     );
251
252     my $object = delete $args{Object};
253
254     return wantarray ? (0, $self->loc("Object passed is not loaded")) : 0
255        unless $object->id;
256
257     # Translate Object to Domain + Instance
258     $args{Domain}   = ref($object) . "-Role";
259     $args{Instance} = $object->id;
260
261     return $self->LoadByCols(%args);
262 }
263
264
265 =head2 LoadTicketRoleGroup  { Ticket => TICKET_ID, Name => TYPE }
266
267 Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>.
268
269 =cut
270
271 sub LoadTicketRoleGroup {
272     my $self = shift;
273     my %args = (
274         Ticket => '0',
275         Name => undef,
276         @_,
277     );
278     RT->Deprecated(
279         Instead => "RT::Group->LoadRoleGroup or RT::Ticket->RoleGroup",
280         Remove => "4.4",
281     );
282     $args{'Name'} = $args{'Type'} if exists $args{'Type'};
283     $self->LoadByCols(
284         Domain   => 'RT::Ticket-Role',
285         Instance => $args{'Ticket'},
286         Name     => $args{'Name'},
287     );
288 }
289
290
291
292 =head2 LoadQueueRoleGroup  { Queue => Queue_ID, Type => TYPE }
293
294 Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>.
295
296 =cut
297
298 sub LoadQueueRoleGroup {
299     my $self = shift;
300     my %args = (
301         Queue => undef,
302         Name => undef,
303         @_,
304     );
305     RT->Deprecated(
306         Instead => "RT::Group->LoadRoleGroup or RT::Queue->RoleGroup",
307         Remove => "4.4",
308     );
309     $args{'Name'} = $args{'Type'} if exists $args{'Type'};
310     $self->LoadByCols(
311         Domain   => 'RT::Queue-Role',
312         Instance => $args{'Queue'},
313         Name     => $args{'Name'},
314     );
315 }
316
317
318
319 =head2 LoadSystemRoleGroup  Name
320
321 Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>.
322
323 =cut
324
325 sub LoadSystemRoleGroup {
326     my $self = shift;
327     my $type = shift;
328     RT->Deprecated(
329         Instead => "RT::Group->LoadRoleGroup or RT::System->RoleGroup",
330         Remove => "4.4",
331     );
332     $self->LoadByCols(
333         Domain   => 'RT::System-Role',
334         Instance => RT::System->Id,
335         Name     => $type
336     );
337 }
338
339 sub LoadByCols {
340     my $self = shift;
341     my %args = ( @_ );
342     if ( exists $args{'Type'} ) {
343         RT->Deprecated( Instead => 'Name', Arguments => 'Type', Remove => '4.4' );
344         $args{'Name'} = $args{'Type'};
345     }
346     return $self->SUPER::LoadByCols( %args );
347 }
348
349
350
351 =head2 Create
352
353 You need to specify what sort of group you're creating by calling one of the other
354 Create_____ routines.
355
356 =cut
357
358 sub Create {
359     my $self = shift;
360     $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
361     return(0,$self->loc('Permission Denied'));
362 }
363
364
365
366 =head2 _Create
367
368 Takes a paramhash with named arguments: Name, Description.
369
370 Returns a tuple of (Id, Message).  If id is 0, the create failed
371
372 =cut
373
374 sub _Create {
375     my $self = shift;
376     my %args = (
377         Name        => undef,
378         Description => undef,
379         Domain      => undef,
380         Instance    => '0',
381         InsideTransaction => undef,
382         _RecordTransaction => 1,
383         @_
384     );
385     if ( $args{'Type'} ) {
386         RT->Deprecated( Instead => 'Name', Arguments => 'Type', Remove => '4.4' );
387         $args{'Name'} = $args{'Type'};
388     } else {
389         $args{'Type'} = $args{'Name'};
390     }
391
392     # Enforce uniqueness on user defined group names
393     if ($args{'Domain'} and $args{'Domain'} eq 'UserDefined') {
394         my ($ok, $msg) = $self->_ValidateUserDefinedName($args{'Name'});
395         return ($ok, $msg) if not $ok;
396     }
397
398     $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
399     # Groups deal with principal ids, rather than user ids.
400     # When creating this group, set up a principal Id for it.
401     my $principal    = RT::Principal->new( $self->CurrentUser );
402     my $principal_id = $principal->Create(
403         PrincipalType => 'Group',
404         ObjectId      => '0'
405     );
406     $principal->__Set(Field => 'ObjectId', Value => $principal_id);
407
408     $self->SUPER::Create(
409         id          => $principal_id,
410         Name        => $args{'Name'},
411         Description => $args{'Description'},
412         Type        => $args{'Type'},
413         Domain      => $args{'Domain'},
414         Instance    => ($args{'Instance'} || '0')
415     );
416     my $id = $self->Id;
417     unless ($id) {
418         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
419         return ( 0, $self->loc('Could not create group') );
420     }
421
422     # If we couldn't create a principal Id, get the fuck out.
423     unless ($principal_id) {
424         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
425         $RT::Logger->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
426         return ( 0, $self->loc('Could not create group') );
427     }
428
429     # Now we make the group a member of itself as a cached group member
430     # this needs to exist so that group ACL checks don't fall over.
431     # you're checking CachedGroupMembers to see if the principal in question
432     # is a member of the principal the rights have been granted too
433
434     # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as 
435     # cached members. thankfully, we're creating the group now...so it has no members.
436     my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
437     $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
438
439
440     if ( $args{'_RecordTransaction'} ) {
441         $self->_NewTransaction( Type => "Create" );
442     }
443
444     $RT::Handle->Commit() unless ($args{'InsideTransaction'});
445
446     return ( $id, $self->loc("Group created") );
447 }
448
449
450
451 =head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
452
453 A helper subroutine which creates a system group 
454
455 Returns a tuple of (Id, Message).  If id is 0, the create failed
456
457 =cut
458
459 sub CreateUserDefinedGroup {
460     my $self = shift;
461
462     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
463         $RT::Logger->warning( $self->CurrentUser->Name
464               . " Tried to create a group without permission." );
465         return ( 0, $self->loc('Permission Denied') );
466     }
467
468     return($self->_Create( Domain => 'UserDefined', Instance => '', @_));
469 }
470
471 =head2 ValidateName VALUE
472
473 Enforces unique user defined group names when updating
474
475 =cut
476
477 sub ValidateName {
478     my ($self, $value) = @_;
479
480     if ($self->Domain and $self->Domain eq 'UserDefined') {
481         my ($ok, $msg) = $self->_ValidateUserDefinedName($value);
482         # It's really too bad we can't pass along the actual error
483         return 0 if not $ok;
484     }
485     return $self->SUPER::ValidateName($value);
486 }
487
488 =head2 _ValidateUserDefinedName VALUE
489
490 Returns true if the user defined group name isn't in use, false otherwise.
491
492 =cut
493
494 sub _ValidateUserDefinedName {
495     my ($self, $value) = @_;
496
497     return (0, 'Name is required') unless length $value;
498
499     my $dupcheck = RT::Group->new(RT->SystemUser);
500     $dupcheck->LoadUserDefinedGroup($value);
501     if ( $dupcheck->id && ( !$self->id || $self->id != $dupcheck->id ) ) {
502         return ( 0, $self->loc( "Group name '[_1]' is already in use", $value ) );
503     }
504     return 1;
505 }
506
507 =head2 _CreateACLEquivalenceGroup { Principal }
508
509 A helper subroutine which creates a group containing only 
510 an individual user. This gets used by the ACL system to check rights.
511 Yes, it denormalizes the data, but that's ok, as we totally win on performance.
512
513 Returns a tuple of (Id, Message).  If id is 0, the create failed
514
515 =cut
516
517 sub _CreateACLEquivalenceGroup { 
518     my $self = shift;
519     my $princ = shift;
520  
521       my $id = $self->_Create( Domain => 'ACLEquivalence', 
522                            Name => 'UserEquiv',
523                            Description => 'ACL equiv. for user '.$princ->Object->Id,
524                            Instance => $princ->Id,
525                            InsideTransaction => 1,
526                            _RecordTransaction => 0 );
527       unless ($id) {
528         $RT::Logger->crit("Couldn't create ACL equivalence group");
529         return undef;
530       }
531     
532        # We use stashuser so we don't get transactions inside transactions
533        # and so we bypass all sorts of cruft we don't need
534        my $aclstash = RT::GroupMember->new($self->CurrentUser);
535        my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
536                                              Member => $princ);
537
538       unless ($stash_id) {
539         $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
540         # We call super delete so we don't get acl checked.
541         $self->SUPER::Delete();
542         return(undef);
543       }
544     return ($id);
545 }
546
547
548
549
550 =head2 CreateRoleGroup
551
552 A convenience method for creating a role group on an object.
553
554 This method expects to be called from B<inside of a database transaction>!  If
555 you're calling it outside of one, you B<MUST> pass a false value for
556 InsideTransaction.
557
558 Takes a paramhash of:
559
560 =over 4
561
562 =item Name
563
564 Required.  RT's core role types are C<Requestor>, C<Cc>, C<AdminCc>, and
565 C<Owner>.  Extensions may add their own.
566
567 =item Object
568
569 Optional.  The object on which this role applies, used to set Domain and
570 Instance automatically.
571
572 =item Domain
573
574 Optional.  The class on which this role applies, with C<-Role> appended.  RT's
575 supported core role group domains are C<RT::Ticket-Role>, C<RT::Queue-Role>,
576 and C<RT::System-Role>.
577
578 Not required if you pass an Object.
579
580 =item Instance
581
582 Optional.  The numeric ID of the object (of the class encoded in Domain) on
583 which this role applies.  If Domain is C<RT::System-Role>, Instance should be C<1>.
584
585 Not required if you pass an Object.
586
587 =item InsideTransaction
588
589 Optional.  Defaults to true in expectation of usual call sites.  If you call
590 this method while not inside a transaction, you C<MUST> pass a false value for
591 this parameter.
592
593 =back
594
595 You must pass either an Object or both Domain and Instance.
596
597 Returns a tuple of (id, Message).  If id is false, the create failed and
598 Message should contain an error string.
599
600 =cut
601
602 sub CreateRoleGroup {
603     my $self = shift;
604     my %args = ( Instance => undef,
605                  Name     => undef,
606                  Domain   => undef,
607                  Object   => undef,
608                  InsideTransaction => 1,
609                  @_ );
610
611     # Translate Object to Domain + Instance
612     my $object = delete $args{Object};
613     if ( $object ) {
614         $args{Domain}   = ref($object) . "-Role";
615         $args{Instance} = $object->id;
616     }
617
618     unless ($args{Instance}) {
619         return ( 0, $self->loc("An Instance must be provided") );
620     }
621
622     unless ($self->ValidateRoleGroup(%args)) {
623         return ( 0, $self->loc("Invalid Group Name and Domain") );
624     }
625
626     if ( exists $args{'Type'} ) {
627         RT->Deprecated( Instead => 'Name', Arguments => 'Type', Remove => '4.4' );
628         $args{'Name'} = $args{'Type'};
629     }
630
631     my %create = map { $_ => $args{$_} } qw(Domain Instance Name);
632
633     my $duplicate = RT::Group->new( RT->SystemUser );
634     $duplicate->LoadByCols( %create );
635     if ($duplicate->id) {
636         return ( 0, $self->loc("Role group exists already") );
637     }
638
639     my ($id, $msg) = $self->_Create(
640         InsideTransaction => $args{InsideTransaction},
641         %create,
642     );
643
644     if ($self->SingleMemberRoleGroup) {
645         $self->_AddMember(
646             PrincipalId => RT->Nobody->Id,
647             InsideTransaction => $args{InsideTransaction},
648             RecordTransaction => 0,
649             Object => $object,
650         );
651     }
652
653     return ($id, $msg);
654 }
655
656 sub RoleClass {
657     my $self = shift;
658     my $domain = shift || $self->Domain;
659     return unless $domain =~ /^(.+)-Role$/;
660     return unless $1->DOES("RT::Record::Role::Roles");
661     return $1;
662 }
663
664 =head2 ValidateRoleGroup
665
666 Takes a param hash containing Domain and Type which are expected to be values
667 passed into L</CreateRoleGroup>.  Returns true if the specified Type is a
668 registered role on the specified Domain.  Otherwise returns false.
669
670 =cut
671
672 sub ValidateRoleGroup {
673     my $self = shift;
674     my %args = (@_);
675     return 0 unless $args{Domain} and ($args{Type} or $args{'Name'});
676
677     my $class = $self->RoleClass($args{Domain});
678     return 0 unless $class;
679
680     return $class->HasRole($args{Type}||$args{'Name'});
681 }
682
683 =head2 SingleMemberRoleGroup
684
685 =cut
686
687 sub SingleMemberRoleGroup {
688     my $self = shift;
689     my $class = $self->RoleClass;
690     return unless $class;
691     return $class->Role($self->Name)->{Single};
692 }
693
694 sub SingleMemberRoleGroupColumn {
695     my $self = shift;
696     my ($class) = $self->Domain =~ /^(.+)-Role$/;
697     return unless $class;
698
699     my $role = $class->Role($self->Name);
700     return unless $role->{Class} eq $class;
701     return $role->{Column};
702 }
703
704 sub RoleGroupObject {
705     my $self = shift;
706     my ($class) = $self->Domain =~ /^(.+)-Role$/;
707     return unless $class;
708     my $obj = $class->new( $self->CurrentUser );
709     $obj->Load( $self->Instance );
710     return $obj;
711 }
712
713 sub Type {
714     my $self = shift;
715     RT->Deprecated( Instead => 'Name', Remove => '4.4' );
716     return $self->_Value('Type', @_);
717 }
718
719 sub SetType {
720     my $self = shift;
721     RT->Deprecated( Instead => 'Name', Remove => '4.4' );
722     return $self->SetName(@_);
723 }
724
725 sub SetName {
726     my $self = shift;
727     my $value = shift;
728
729     my ($status, $msg) = $self->_Set( Field => 'Name', Value => $value );
730     return ($status, $msg) unless $status;
731
732     {
733         my ($status, $msg) = $self->__Set( Field => 'Type', Value => $value );
734         RT->Logger->error("Couldn't set Type: $msg") unless $status;
735     }
736
737     return ($status, $msg);
738 }
739
740 =head2 Delete
741
742 Delete this object
743
744 =cut
745
746 sub Delete {
747     my $self = shift;
748
749     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
750         return ( 0, 'Permission Denied' );
751     }
752
753     $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
754     # TODO XXX 
755    
756     # Remove the principal object
757     # Remove this group from anything it's a member of.
758     # Remove all cached members of this group
759     # Remove any rights granted to this group
760     # remove any rights delegated by way of this group
761
762     return ( $self->SUPER::Delete(@_) );
763 }
764
765
766 =head2 SetDisabled BOOL
767
768 If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
769 It will not appear in most group listings.
770
771 This routine finds all the cached group members that are members of this group  (recursively) and disables them.
772
773 =cut 
774
775  # }}}
776
777  sub SetDisabled {
778      my $self = shift;
779      my $val = shift;
780      unless ( $self->CurrentUserHasRight('AdminGroup') ) {
781         return (0, $self->loc('Permission Denied'));
782     }
783     $RT::Handle->BeginTransaction();
784     $self->PrincipalObj->SetDisabled($val);
785
786
787
788
789     # Find all occurrences of this member as a member of this group
790     # in the cache and nuke them, recursively.
791
792     # The following code will delete all Cached Group members
793     # where this member's group is _not_ the primary group 
794     # (Ie if we're deleting C as a member of B, and B happens to be 
795     # a member of A, will delete C as a member of A without touching
796     # C as a member of B
797
798     my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
799
800     $cached_submembers->Limit( FIELD    => 'ImmediateParentId', OPERATOR => '=', VALUE    => $self->Id);
801
802     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
803     # TODO what about the groups key cache?
804     RT::Principal->InvalidateACLCache();
805
806
807
808     while ( my $item = $cached_submembers->Next() ) {
809         my $del_err = $item->SetDisabled($val);
810         unless ($del_err) {
811             $RT::Handle->Rollback();
812             $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
813             return (undef);
814         }
815     }
816
817     $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
818
819     $RT::Handle->Commit();
820     if ( $val == 1 ) {
821         return (1, $self->loc("Group disabled"));
822     } else {
823         return (1, $self->loc("Group enabled"));
824     }
825
826 }
827
828
829
830
831 sub Disabled {
832     my $self = shift;
833     $self->PrincipalObj->Disabled(@_);
834 }
835
836
837
838 =head2 DeepMembersObj
839
840 Returns an RT::CachedGroupMembers object of this group's members,
841 including all members of subgroups.
842
843 =cut
844
845 sub DeepMembersObj {
846     my $self = shift;
847     my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
848
849     #If we don't have rights, don't include any results
850     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
851     $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
852
853     return ( $members_obj );
854
855 }
856
857
858
859 =head2 MembersObj
860
861 Returns an RT::GroupMembers object of this group's direct members.
862
863 =cut
864
865 sub MembersObj {
866     my $self = shift;
867     my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
868
869     #If we don't have rights, don't include any results
870     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
871     $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
872
873     return ( $members_obj );
874
875 }
876
877
878
879 =head2 GroupMembersObj [Recursively => 1]
880
881 Returns an L<RT::Groups> object of this group's members.
882 By default returns groups including all subgroups, but
883 could be changed with C<Recursively> named argument.
884
885 B<Note> that groups are not filtered by type and result
886 may contain as well system groups and others.
887
888 =cut
889
890 sub GroupMembersObj {
891     my $self = shift;
892     my %args = ( Recursively => 1, @_ );
893
894     my $groups = RT::Groups->new( $self->CurrentUser );
895     my $members_table = $args{'Recursively'}?
896         'CachedGroupMembers': 'GroupMembers';
897
898     my $members_alias = $groups->NewAlias( $members_table );
899     $groups->Join(
900         ALIAS1 => $members_alias,           FIELD1 => 'MemberId',
901         ALIAS2 => $groups->PrincipalsAlias, FIELD2 => 'id',
902     );
903     $groups->Limit(
904         ALIAS    => $members_alias,
905         FIELD    => 'GroupId',
906         VALUE    => $self->PrincipalId,
907     );
908     $groups->Limit(
909         ALIAS => $members_alias,
910         FIELD => 'Disabled',
911         VALUE => 0,
912     ) if $args{'Recursively'};
913
914     return $groups;
915 }
916
917
918
919 =head2 UserMembersObj
920
921 Returns an L<RT::Users> object of this group's members, by default
922 returns users including all members of subgroups, but could be
923 changed with C<Recursively> named argument.
924
925 =cut
926
927 sub UserMembersObj {
928     my $self = shift;
929     my %args = ( Recursively => 1, @_ );
930
931     #If we don't have rights, don't include any results
932     # TODO XXX  WHY IS THERE NO ACL CHECK HERE?
933
934     my $members_table = $args{'Recursively'}?
935         'CachedGroupMembers': 'GroupMembers';
936
937     my $users = RT::Users->new($self->CurrentUser);
938     my $members_alias = $users->NewAlias( $members_table );
939     $users->Join(
940         ALIAS1 => $members_alias,           FIELD1 => 'MemberId',
941         ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id',
942     );
943     $users->Limit(
944         ALIAS => $members_alias,
945         FIELD => 'GroupId',
946         VALUE => $self->PrincipalId,
947     );
948     $users->Limit(
949         ALIAS => $members_alias,
950         FIELD => 'Disabled',
951         VALUE => 0,
952     ) if $args{'Recursively'};
953
954     return ( $users);
955 }
956
957
958
959 =head2 MemberEmailAddresses
960
961 Returns an array of the email addresses of all of this group's members
962
963
964 =cut
965
966 sub MemberEmailAddresses {
967     my $self = shift;
968     return sort grep defined && length,
969         map $_->EmailAddress,
970         @{ $self->UserMembersObj->ItemsArrayRef };
971 }
972
973
974
975 =head2 MemberEmailAddressesAsString
976
977 Returns a comma delimited string of the email addresses of all users 
978 who are members of this group.
979
980 =cut
981
982
983 sub MemberEmailAddressesAsString {
984     my $self = shift;
985     return (join(', ', $self->MemberEmailAddresses));
986 }
987
988
989
990 =head2 AddMember PRINCIPAL_ID
991
992 AddMember adds a principal to this group.  It takes a single principal id.
993 Returns a two value array. the first value is true on successful 
994 addition or 0 on failure.  The second value is a textual status msg.
995
996 =cut
997
998 sub AddMember {
999     my $self       = shift;
1000     my $new_member = shift;
1001
1002
1003
1004     # We should only allow membership changes if the user has the right 
1005     # to modify group membership or the user is the principal in question
1006     # and the user has the right to modify his own membership
1007     unless ( ($new_member == $self->CurrentUser->PrincipalId &&
1008               $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1009               $self->CurrentUserHasRight('AdminGroupMembership') ) {
1010         #User has no permission to be doing this
1011         return ( 0, $self->loc("Permission Denied") );
1012     }
1013
1014     $self->_AddMember(PrincipalId => $new_member);
1015 }
1016
1017 # A helper subroutine for AddMember that bypasses the ACL checks
1018 # this should _ONLY_ ever be called from Ticket/Queue AddWatcher
1019 # when we want to deal with groups according to queue rights
1020 # In the dim future, this will all get factored out and life
1021 # will get better
1022
1023 # takes a paramhash of { PrincipalId => undef, InsideTransaction }
1024
1025 sub _AddMember {
1026     my $self = shift;
1027     my %args = ( PrincipalId => undef,
1028                  InsideTransaction => undef,
1029                  RecordTransaction => 1,
1030                  @_);
1031
1032     # RecordSetTransaction is used by _DeleteMember to get one txn but not the other
1033     $args{RecordSetTransaction} = $args{RecordTransaction}
1034         unless exists $args{RecordSetTransaction};
1035
1036     my $new_member = $args{'PrincipalId'};
1037
1038     unless ($self->Id) {
1039         $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
1040         return(0, $self->loc("Group not found"));
1041     }
1042
1043     unless ($new_member =~ /^\d+$/) {
1044         $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
1045     }
1046
1047
1048     my $new_member_obj = RT::Principal->new( $self->CurrentUser );
1049     $new_member_obj->Load($new_member);
1050
1051
1052     unless ( $new_member_obj->Id ) {
1053         $RT::Logger->debug("Couldn't find that principal");
1054         return ( 0, $self->loc("Couldn't find that principal") );
1055     }
1056
1057     if ( $self->HasMember( $new_member_obj ) ) {
1058
1059         #User is already a member of this group. no need to add it
1060         return ( 0, $self->loc("Group already has member: [_1]", $new_member_obj->Object->Name) );
1061     }
1062     if ( $new_member_obj->IsGroup &&
1063          $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
1064
1065         #This group can't be made to be a member of itself
1066         return ( 0, $self->loc("Groups can't be members of their members"));
1067     }
1068
1069     my @purge;
1070     push @purge, @{$self->MembersObj->ItemsArrayRef}
1071         if $self->SingleMemberRoleGroup;
1072
1073     my $member_object = RT::GroupMember->new( $self->CurrentUser );
1074     my $id = $member_object->Create(
1075         Member => $new_member_obj,
1076         Group => $self->PrincipalObj,
1077         InsideTransaction => $args{'InsideTransaction'}
1078     );
1079
1080     return(0, $self->loc("Couldn't add member to group"))
1081         unless $id;
1082
1083     # Purge all previous members (we're a single member role group)
1084     my $old_member_id;
1085     for my $member (@purge) {
1086         my $old_member = $member->MemberId;
1087         my ($ok, $msg) = $member->Delete();
1088         return(0, $self->loc("Couldn't remove previous member: [_1]", $msg))
1089             unless $ok;
1090
1091         # We remove all members in this loop, but there should only ever be one
1092         # member.  Keep track of the last one successfully removed for the
1093         # SetWatcher transaction below.
1094         $old_member_id = $old_member;
1095     }
1096
1097     # Update the column
1098     if (my $col = $self->SingleMemberRoleGroupColumn) {
1099         my $obj = $args{Object} || $self->RoleGroupObject;
1100         my ($ok, $msg) = $obj->_Set(
1101             Field    => $col,
1102             Value    => $new_member_obj->Id,
1103             CheckACL => 0,                  # don't check acl
1104             RecordTransaction => $args{'RecordSetTransaction'},
1105         );
1106         return (0, $self->loc("Could not update column [_1]: [_2]", $col, $msg))
1107             unless $ok;
1108     }
1109
1110     # Record transactions for UserDefined groups
1111     if ($args{RecordTransaction} && $self->Domain eq 'UserDefined') {
1112         $new_member_obj->Object->_NewTransaction(
1113             Type  => 'AddMembership',
1114             Field => $self->PrincipalObj->id,
1115         );
1116
1117         $self->_NewTransaction(
1118             Type  => 'AddMember',
1119             Field => $new_member,
1120         );
1121     }
1122
1123     # Record an Add/SetWatcher txn on the object if we're a role group
1124     if ($args{RecordTransaction} and $self->RoleClass) {
1125         my $obj = $args{Object} || $self->RoleGroupObject;
1126
1127         if ($self->SingleMemberRoleGroup) {
1128             $obj->_NewTransaction(
1129                 Type     => 'SetWatcher',
1130                 OldValue => $old_member_id,
1131                 NewValue => $new_member_obj->Id,
1132                 Field    => $self->Name,
1133             );
1134         } else {
1135             $obj->_NewTransaction(
1136                 Type     => 'AddWatcher', # use "watcher" for history's sake
1137                 NewValue => $new_member_obj->Id,
1138                 Field    => $self->Name,
1139             );
1140         }
1141     }
1142
1143     return (1, $self->loc("[_1] set to [_2]",
1144                           $self->loc($self->Name), $new_member_obj->Object->Name) )
1145         if $self->SingleMemberRoleGroup;
1146
1147     return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
1148 }
1149
1150
1151 =head2 HasMember RT::Principal|id
1152
1153 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a 
1154 member of this group.
1155 Returns undef if the user isn't a member of the group or if the current
1156 user doesn't have permission to find out. Arguably, it should differentiate
1157 between ACL failure and non membership.
1158
1159 =cut
1160
1161 sub HasMember {
1162     my $self    = shift;
1163     my $principal = shift;
1164
1165     my $id;
1166     if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1167         $id = $principal->id;
1168     } elsif ( $principal =~ /^\d+$/ ) {
1169         $id = $principal;
1170     } else {
1171         $RT::Logger->error("Group::HasMember was called with an argument that".
1172                           " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
1173         return(undef);
1174     }
1175     return undef unless $id;
1176
1177     my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1178     $member_obj->LoadByCols(
1179         MemberId => $id, 
1180         GroupId  => $self->PrincipalId
1181     );
1182
1183     if ( my $member_id = $member_obj->id ) {
1184         return $member_id;
1185     }
1186     else {
1187         return (undef);
1188     }
1189 }
1190
1191
1192
1193 =head2 HasMemberRecursively RT::Principal|id
1194
1195 Takes an L<RT::Principal> object or its id and returns true if that user is a member of 
1196 this group.
1197 Returns undef if the user isn't a member of the group or if the current
1198 user doesn't have permission to find out. Arguably, it should differentiate
1199 between ACL failure and non membership.
1200
1201 =cut
1202
1203 sub HasMemberRecursively {
1204     my $self    = shift;
1205     my $principal = shift;
1206
1207     my $id;
1208     if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1209         $id = $principal->id;
1210     } elsif ( $principal =~ /^\d+$/ ) {
1211         $id = $principal;
1212     } else {
1213         $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1214                           " isn't an RT::Principal or id. It's $principal");
1215         return(undef);
1216     }
1217     return undef unless $id;
1218
1219     my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1220     $member_obj->LoadByCols(
1221         MemberId => $id, 
1222         GroupId  => $self->PrincipalId
1223     );
1224
1225     if ( my $member_id = $member_obj->id ) {
1226         return $member_id;
1227     }
1228     else {
1229         return (undef);
1230     }
1231 }
1232
1233
1234
1235 =head2 DeleteMember PRINCIPAL_ID
1236
1237 Takes the principal id of a current user or group.
1238 If the current user has apropriate rights,
1239 removes that GroupMember from this group.
1240 Returns a two value array. the first value is true on successful 
1241 addition or 0 on failure.  The second value is a textual status msg.
1242
1243 Optionally takes a hash of key value flags, such as RecordTransaction.
1244
1245 =cut
1246
1247 sub DeleteMember {
1248     my $self   = shift;
1249     my $member_id = shift;
1250
1251
1252     # We should only allow membership changes if the user has the right 
1253     # to modify group membership or the user is the principal in question
1254     # and the user has the right to modify his own membership
1255
1256     unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1257               $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1258               $self->CurrentUserHasRight('AdminGroupMembership') ) {
1259         #User has no permission to be doing this
1260         return ( 0, $self->loc("Permission Denied") );
1261     }
1262     $self->_DeleteMember($member_id, @_);
1263 }
1264
1265 # A helper subroutine for DeleteMember that bypasses the ACL checks
1266 # this should _ONLY_ ever be called from Ticket/Queue  DeleteWatcher
1267 # when we want to deal with groups according to queue rights
1268 # In the dim future, this will all get factored out and life
1269 # will get better
1270
1271 sub _DeleteMember {
1272     my $self = shift;
1273     my $member_id = shift;
1274     my %args = (
1275         RecordTransaction   => 1,
1276         @_,
1277     );
1278
1279
1280     my $member_obj =  RT::GroupMember->new( $self->CurrentUser );
1281     
1282     $member_obj->LoadByCols( MemberId  => $member_id,
1283                              GroupId => $self->PrincipalId);
1284
1285
1286     #If we couldn't load it, return undef.
1287     unless ( $member_obj->Id() ) {
1288         $RT::Logger->debug("Group has no member with that id");
1289         return ( 0,$self->loc( "Group has no such member" ));
1290     }
1291
1292     my $old_member = $member_obj->MemberId;
1293
1294     #Now that we've checked ACLs and sanity, delete the groupmember
1295     my $val = $member_obj->Delete();
1296
1297     unless ($val) {
1298         $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1299         return ( 0, $self->loc("Member not deleted" ));
1300     }
1301
1302     if ($self->RoleClass) {
1303         my %txn = (
1304             OldValue => $old_member,
1305             Field    => $self->Name,
1306         );
1307
1308         if ($self->SingleMemberRoleGroup) {
1309             # _AddMember creates the Set-Owner txn (for example) but we handle
1310             # the SetWatcher-Owner txn below.
1311             $self->_AddMember(
1312                 PrincipalId             => RT->Nobody->Id,
1313                 RecordTransaction       => 0,
1314                 RecordSetTransaction    => $args{RecordTransaction},
1315             );
1316             $txn{Type}     = "SetWatcher";
1317             $txn{NewValue} = RT->Nobody->id;
1318         } else {
1319             $txn{Type} = "DelWatcher";
1320         }
1321
1322         if ($args{RecordTransaction}) {
1323             my $obj = $args{Object} || $self->RoleGroupObject;
1324             $obj->_NewTransaction(%txn);
1325         }
1326     }
1327
1328     # Record transactions for UserDefined groups
1329     if ($args{RecordTransaction} && $self->Domain eq 'UserDefined') {
1330         $member_obj->MemberObj->Object->_NewTransaction(
1331             Type  => 'DeleteMembership',
1332             Field => $self->PrincipalObj->id,
1333         );
1334
1335         $self->_NewTransaction(
1336             Type  => 'DeleteMember',
1337             Field => $member_id,
1338         );
1339     }
1340
1341     return ( $val, $self->loc("Member deleted") );
1342 }
1343
1344
1345
1346 sub _Set {
1347     my $self = shift;
1348     my %args = (
1349         Field => undef,
1350         Value => undef,
1351         TransactionType   => 'Set',
1352         RecordTransaction => 1,
1353         @_
1354     );
1355
1356     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1357         return ( 0, $self->loc('Permission Denied') );
1358         }
1359
1360     my $Old = $self->SUPER::_Value("$args{'Field'}");
1361
1362     my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1363                                           Value => $args{'Value'} );
1364
1365     #If we can't actually set the field to the value, don't record
1366     # a transaction. instead, get out of here.
1367     if ( $ret == 0 ) { return ( 0, $msg ); }
1368
1369     if ( $args{'RecordTransaction'} == 1 ) {
1370
1371         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1372                                                Type => $args{'TransactionType'},
1373                                                Field     => $args{'Field'},
1374                                                NewValue  => $args{'Value'},
1375                                                OldValue  => $Old,
1376                                                TimeTaken => $args{'TimeTaken'},
1377         );
1378         return ( $Trans, scalar $TransObj->Description );
1379     }
1380     else {
1381         return ( $ret, $msg );
1382     }
1383 }
1384
1385 =head2 CurrentUserCanSee
1386
1387 Always returns 1; unfortunately, for historical reasons, users have
1388 always been able to examine groups they have indirect access to, even if
1389 they do not have SeeGroup explicitly.
1390
1391 =cut
1392
1393 sub CurrentUserCanSee {
1394     my $self = shift;
1395     return 1;
1396 }
1397
1398
1399 =head2 PrincipalObj
1400
1401 Returns the principal object for this user. returns an empty RT::Principal
1402 if there's no principal object matching this user. 
1403 The response is cached. PrincipalObj should never ever change.
1404
1405
1406 =cut
1407
1408
1409 sub PrincipalObj {
1410     my $self = shift;
1411     my $res = RT::Principal->new( $self->CurrentUser );
1412     $res->Load( $self->id );
1413     return $res;
1414 }
1415
1416
1417 =head2 PrincipalId  
1418
1419 Returns this user's PrincipalId
1420
1421 =cut
1422
1423 sub PrincipalId {
1424     my $self = shift;
1425     return $self->Id;
1426 }
1427
1428 sub InstanceObj {
1429     my $self = shift;
1430
1431     my $class;
1432     if ( $self->Domain eq 'ACLEquivalence' ) {
1433         $class = "RT::User";
1434     } elsif ($self->Domain eq 'RT::Queue-Role') {
1435         $class = "RT::Queue";
1436     } elsif ($self->Domain eq 'RT::Ticket-Role') {
1437         $class = "RT::Ticket";
1438     }
1439
1440     return unless $class;
1441
1442     my $obj = $class->new( $self->CurrentUser );
1443     $obj->Load( $self->Instance );
1444     return $obj;
1445 }
1446
1447 sub BasicColumns {
1448     (
1449         [ Name => 'Name' ],
1450         [ Description => 'Description' ],
1451     );
1452 }
1453
1454
1455 =head1 AUTHOR
1456
1457 Jesse Vincent, jesse@bestpractical.com
1458
1459 =head1 SEE ALSO
1460
1461 RT
1462
1463 =cut
1464
1465
1466
1467
1468
1469 =head2 id
1470
1471 Returns the current value of id.
1472 (In the database, id is stored as int(11).)
1473
1474
1475 =cut
1476
1477
1478 =head2 Name
1479
1480 Returns the current value of Name.
1481 (In the database, Name is stored as varchar(200).)
1482
1483
1484
1485 =head2 SetName VALUE
1486
1487
1488 Set Name to VALUE.
1489 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1490 (In the database, Name will be stored as a varchar(200).)
1491
1492
1493 =cut
1494
1495
1496 =head2 Description
1497
1498 Returns the current value of Description.
1499 (In the database, Description is stored as varchar(255).)
1500
1501
1502
1503 =head2 SetDescription VALUE
1504
1505
1506 Set Description to VALUE.
1507 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1508 (In the database, Description will be stored as a varchar(255).)
1509
1510
1511 =cut
1512
1513
1514 =head2 Domain
1515
1516 Returns the current value of Domain.
1517 (In the database, Domain is stored as varchar(64).)
1518
1519
1520
1521 =head2 SetDomain VALUE
1522
1523
1524 Set Domain to VALUE.
1525 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1526 (In the database, Domain will be stored as a varchar(64).)
1527
1528
1529 =cut
1530
1531
1532 =head2 Type
1533
1534 Returns the current value of Type.
1535 (In the database, Type is stored as varchar(64).)
1536
1537 Deprecated, use Name instead, will be removed in 4.4.
1538
1539 =head2 SetType VALUE
1540
1541
1542 Set Type to VALUE.
1543 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1544 (In the database, Type will be stored as a varchar(64).)
1545
1546 Deprecated, use SetName instead, will be removed in 4.4.
1547
1548 =cut
1549
1550
1551 =head2 Instance
1552
1553 Returns the current value of Instance.
1554 (In the database, Instance is stored as int(11).)
1555
1556
1557
1558 =head2 SetInstance VALUE
1559
1560
1561 Set Instance to VALUE.
1562 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1563 (In the database, Instance will be stored as a int(11).)
1564
1565
1566 =cut
1567
1568
1569 =head2 Creator
1570
1571 Returns the current value of Creator.
1572 (In the database, Creator is stored as int(11).)
1573
1574
1575 =cut
1576
1577
1578 =head2 Created
1579
1580 Returns the current value of Created.
1581 (In the database, Created is stored as datetime.)
1582
1583
1584 =cut
1585
1586
1587 =head2 LastUpdatedBy
1588
1589 Returns the current value of LastUpdatedBy.
1590 (In the database, LastUpdatedBy is stored as int(11).)
1591
1592
1593 =cut
1594
1595
1596 =head2 LastUpdated
1597
1598 Returns the current value of LastUpdated.
1599 (In the database, LastUpdated is stored as datetime.)
1600
1601
1602 =cut
1603
1604
1605
1606 sub _CoreAccessible {
1607     {
1608
1609         id =>
1610                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1611         Name =>
1612                 {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
1613         Description =>
1614                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1615         Domain =>
1616                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1617         Type =>
1618                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1619         Instance =>
1620                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1621         Creator =>
1622                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1623         Created =>
1624                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1625         LastUpdatedBy =>
1626                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1627         LastUpdated =>
1628                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1629
1630  }
1631 };
1632
1633 sub FindDependencies {
1634     my $self = shift;
1635     my ($walker, $deps) = @_;
1636
1637     $self->SUPER::FindDependencies($walker, $deps);
1638
1639     my $instance = $self->InstanceObj;
1640     $deps->Add( out => $instance ) if $instance;
1641
1642     # Group members records, unless we're a system group
1643     if ($self->Domain ne "SystemInternal") {
1644         my $objs = RT::GroupMembers->new( $self->CurrentUser );
1645         $objs->LimitToMembersOfGroup( $self->PrincipalId );
1646         $deps->Add( in => $objs );
1647     }
1648
1649     # Group member records group belongs to
1650     my $objs = RT::GroupMembers->new( $self->CurrentUser );
1651     $objs->Limit( FIELD => 'MemberId', VALUE => $self->PrincipalId );
1652     $deps->Add( in => $objs );
1653 }
1654
1655 sub __DependsOn {
1656     my $self = shift;
1657     my %args = (
1658         Shredder => undef,
1659         Dependencies => undef,
1660         @_,
1661     );
1662     my $deps = $args{'Dependencies'};
1663     my $list = [];
1664
1665 # User is inconsistent without own Equivalence group
1666     if( $self->Domain eq 'ACLEquivalence' ) {
1667         # delete user entry after ACL equiv group
1668         # in other case we will get deep recursion
1669         my $objs = RT::User->new($self->CurrentUser);
1670         $objs->Load( $self->Instance );
1671         $deps->_PushDependency(
1672             BaseObject => $self,
1673             Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER,
1674             TargetObject => $objs,
1675             Shredder => $args{'Shredder'}
1676         );
1677     }
1678
1679 # Principal
1680     $deps->_PushDependency(
1681         BaseObject => $self,
1682         Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER,
1683         TargetObject => $self->PrincipalObj,
1684         Shredder => $args{'Shredder'}
1685     );
1686
1687 # Group members records
1688     my $objs = RT::GroupMembers->new( $self->CurrentUser );
1689     $objs->LimitToMembersOfGroup( $self->PrincipalId );
1690     push( @$list, $objs );
1691
1692 # Group member records group belongs to
1693     $objs = RT::GroupMembers->new( $self->CurrentUser );
1694     $objs->Limit(
1695         VALUE => $self->PrincipalId,
1696         FIELD => 'MemberId',
1697         ENTRYAGGREGATOR => 'OR',
1698         QUOTEVALUE => 0
1699     );
1700     push( @$list, $objs );
1701
1702 # Cached group members records
1703     push( @$list, $self->DeepMembersObj );
1704
1705 # Cached group member records group belongs to
1706     $objs = RT::GroupMembers->new( $self->CurrentUser );
1707     $objs->Limit(
1708         VALUE => $self->PrincipalId,
1709         FIELD => 'MemberId',
1710         ENTRYAGGREGATOR => 'OR',
1711         QUOTEVALUE => 0
1712     );
1713     push( @$list, $objs );
1714
1715 # Cleanup group's membership transactions
1716     $objs = RT::Transactions->new( $self->CurrentUser );
1717     $objs->Limit( FIELD => 'Type', OPERATOR => 'IN', VALUE => ['AddMember', 'DeleteMember'] );
1718     $objs->Limit( FIELD => 'Field', VALUE => $self->PrincipalObj->id, ENTRYAGGREGATOR => 'AND' );
1719     push( @$list, $objs );
1720
1721     $deps->_PushDependencies(
1722         BaseObject => $self,
1723         Flags => RT::Shredder::Constants::DEPENDS_ON,
1724         TargetObjects => $list,
1725         Shredder => $args{'Shredder'}
1726     );
1727     return $self->SUPER::__DependsOn( %args );
1728 }
1729
1730 sub BeforeWipeout {
1731     my $self = shift;
1732     if( $self->Domain eq 'SystemInternal' ) {
1733         RT::Shredder::Exception::Info->throw('SystemObject');
1734     }
1735     return $self->SUPER::BeforeWipeout( @_ );
1736 }
1737
1738 sub Serialize {
1739     my $self = shift;
1740     my %args = (@_);
1741     my %store = $self->SUPER::Serialize(@_);
1742
1743     my $instance = $self->InstanceObj;
1744     $store{Instance} = \($instance->UID) if $instance;
1745
1746     $store{Disabled} = $self->PrincipalObj->Disabled;
1747     $store{Principal} = $self->PrincipalObj->UID;
1748     $store{PrincipalId} = $self->PrincipalObj->Id;
1749     return %store;
1750 }
1751
1752 sub PreInflate {
1753     my $class = shift;
1754     my ($importer, $uid, $data) = @_;
1755
1756     my $principal_uid = delete $data->{Principal};
1757     my $principal_id  = delete $data->{PrincipalId};
1758     my $disabled      = delete $data->{Disabled};
1759
1760     # Inflate refs into their IDs
1761     $class->SUPER::PreInflate( $importer, $uid, $data );
1762
1763     # Factored out code, in case we find an existing version of this group
1764     my $obj = RT::Group->new( RT->SystemUser );
1765     my $duplicated = sub {
1766         $importer->SkipTransactions( $uid );
1767         $importer->Resolve(
1768             $principal_uid,
1769             ref($obj->PrincipalObj),
1770             $obj->PrincipalObj->Id
1771         );
1772         $importer->Resolve( $uid => ref($obj), $obj->Id );
1773         return;
1774     };
1775
1776     # Go looking for the pre-existing version of it
1777     if ($data->{Domain} eq "ACLEquivalence") {
1778         $obj->LoadACLEquivalenceGroup( $data->{Instance} );
1779         return $duplicated->() if $obj->Id;
1780
1781         # Update description for the new ID
1782         $data->{Description} = 'ACL equiv. for user '.$data->{Instance};
1783     } elsif ($data->{Domain} eq "UserDefined") {
1784         $data->{Name} = $importer->Qualify($data->{Name});
1785         $obj->LoadUserDefinedGroup( $data->{Name} );
1786         if ($obj->Id) {
1787             $importer->MergeValues($obj, $data);
1788             return $duplicated->();
1789         }
1790     } elsif ($data->{Domain} =~ /^(SystemInternal|RT::System-Role)$/) {
1791         $obj->LoadByCols( Domain => $data->{Domain}, Name => $data->{Name} );
1792         return $duplicated->() if $obj->Id;
1793     } elsif ($data->{Domain} eq "RT::Queue-Role") {
1794         my $queue = RT::Queue->new( RT->SystemUser );
1795         $queue->Load( $data->{Instance} );
1796         $obj->LoadRoleGroup( Object => $queue, Name => $data->{Name} );
1797         return $duplicated->() if $obj->Id;
1798     }
1799
1800     my $principal = RT::Principal->new( RT->SystemUser );
1801     my ($id) = $principal->Create(
1802         PrincipalType => 'Group',
1803         Disabled => $disabled,
1804         ObjectId => 0,
1805     );
1806
1807     # Now we have a principal id, set the id for the group record
1808     $data->{id} = $id;
1809
1810     $importer->Resolve( $principal_uid => ref($principal), $id );
1811
1812     $importer->Postpone(
1813         for => $uid,
1814         uid => $principal_uid,
1815         column => "ObjectId",
1816     );
1817
1818     return 1;
1819 }
1820
1821 sub PostInflate {
1822     my $self = shift;
1823
1824     my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
1825     $cgm->Create(
1826         Group  => $self->PrincipalObj,
1827         Member => $self->PrincipalObj,
1828         ImmediateParent => $self->PrincipalObj
1829     );
1830 }
1831
1832 RT::Base->_ImportOverlays();
1833
1834 1;