]> git.uio.no Git - usit-rt.git/blob - lib/RT/Group.pm
Master to 4.2.8
[usit-rt.git] / lib / RT / Group.pm
1
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2014 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 an Add/SetWatcher txn on the object if we're a role group
1111     if ($args{RecordTransaction} and $self->RoleClass) {
1112         my $obj = $args{Object} || $self->RoleGroupObject;
1113
1114         if ($self->SingleMemberRoleGroup) {
1115             $obj->_NewTransaction(
1116                 Type     => 'SetWatcher',
1117                 OldValue => $old_member_id,
1118                 NewValue => $new_member_obj->Id,
1119                 Field    => $self->Name,
1120             );
1121         } else {
1122             $obj->_NewTransaction(
1123                 Type     => 'AddWatcher', # use "watcher" for history's sake
1124                 NewValue => $new_member_obj->Id,
1125                 Field    => $self->Name,
1126             );
1127         }
1128     }
1129
1130     return (1, $self->loc("[_1] set to [_2]",
1131                           $self->loc($self->Name), $new_member_obj->Object->Name) )
1132         if $self->SingleMemberRoleGroup;
1133
1134     return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
1135 }
1136
1137
1138 =head2 HasMember RT::Principal|id
1139
1140 Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a 
1141 member of this group.
1142 Returns undef if the user isn't a member of the group or if the current
1143 user doesn't have permission to find out. Arguably, it should differentiate
1144 between ACL failure and non membership.
1145
1146 =cut
1147
1148 sub HasMember {
1149     my $self    = shift;
1150     my $principal = shift;
1151
1152     my $id;
1153     if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1154         $id = $principal->id;
1155     } elsif ( $principal =~ /^\d+$/ ) {
1156         $id = $principal;
1157     } else {
1158         $RT::Logger->error("Group::HasMember was called with an argument that".
1159                           " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
1160         return(undef);
1161     }
1162     return undef unless $id;
1163
1164     my $member_obj = RT::GroupMember->new( $self->CurrentUser );
1165     $member_obj->LoadByCols(
1166         MemberId => $id, 
1167         GroupId  => $self->PrincipalId
1168     );
1169
1170     if ( my $member_id = $member_obj->id ) {
1171         return $member_id;
1172     }
1173     else {
1174         return (undef);
1175     }
1176 }
1177
1178
1179
1180 =head2 HasMemberRecursively RT::Principal|id
1181
1182 Takes an L<RT::Principal> object or its id and returns true if that user is a member of 
1183 this group.
1184 Returns undef if the user isn't a member of the group or if the current
1185 user doesn't have permission to find out. Arguably, it should differentiate
1186 between ACL failure and non membership.
1187
1188 =cut
1189
1190 sub HasMemberRecursively {
1191     my $self    = shift;
1192     my $principal = shift;
1193
1194     my $id;
1195     if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
1196         $id = $principal->id;
1197     } elsif ( $principal =~ /^\d+$/ ) {
1198         $id = $principal;
1199     } else {
1200         $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
1201                           " isn't an RT::Principal or id. It's $principal");
1202         return(undef);
1203     }
1204     return undef unless $id;
1205
1206     my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
1207     $member_obj->LoadByCols(
1208         MemberId => $id, 
1209         GroupId  => $self->PrincipalId
1210     );
1211
1212     if ( my $member_id = $member_obj->id ) {
1213         return $member_id;
1214     }
1215     else {
1216         return (undef);
1217     }
1218 }
1219
1220
1221
1222 =head2 DeleteMember PRINCIPAL_ID
1223
1224 Takes the principal id of a current user or group.
1225 If the current user has apropriate rights,
1226 removes that GroupMember from this group.
1227 Returns a two value array. the first value is true on successful 
1228 addition or 0 on failure.  The second value is a textual status msg.
1229
1230 Optionally takes a hash of key value flags, such as RecordTransaction.
1231
1232 =cut
1233
1234 sub DeleteMember {
1235     my $self   = shift;
1236     my $member_id = shift;
1237
1238
1239     # We should only allow membership changes if the user has the right 
1240     # to modify group membership or the user is the principal in question
1241     # and the user has the right to modify his own membership
1242
1243     unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
1244               $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
1245               $self->CurrentUserHasRight('AdminGroupMembership') ) {
1246         #User has no permission to be doing this
1247         return ( 0, $self->loc("Permission Denied") );
1248     }
1249     $self->_DeleteMember($member_id, @_);
1250 }
1251
1252 # A helper subroutine for DeleteMember that bypasses the ACL checks
1253 # this should _ONLY_ ever be called from Ticket/Queue  DeleteWatcher
1254 # when we want to deal with groups according to queue rights
1255 # In the dim future, this will all get factored out and life
1256 # will get better
1257
1258 sub _DeleteMember {
1259     my $self = shift;
1260     my $member_id = shift;
1261     my %args = (
1262         RecordTransaction   => 1,
1263         @_,
1264     );
1265
1266
1267     my $member_obj =  RT::GroupMember->new( $self->CurrentUser );
1268     
1269     $member_obj->LoadByCols( MemberId  => $member_id,
1270                              GroupId => $self->PrincipalId);
1271
1272
1273     #If we couldn't load it, return undef.
1274     unless ( $member_obj->Id() ) {
1275         $RT::Logger->debug("Group has no member with that id");
1276         return ( 0,$self->loc( "Group has no such member" ));
1277     }
1278
1279     my $old_member = $member_obj->MemberId;
1280
1281     #Now that we've checked ACLs and sanity, delete the groupmember
1282     my $val = $member_obj->Delete();
1283
1284     unless ($val) {
1285         $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
1286         return ( 0, $self->loc("Member not deleted" ));
1287     }
1288
1289     if ($self->RoleClass) {
1290         my %txn = (
1291             OldValue => $old_member,
1292             Field    => $self->Name,
1293         );
1294
1295         if ($self->SingleMemberRoleGroup) {
1296             # _AddMember creates the Set-Owner txn (for example) but we handle
1297             # the SetWatcher-Owner txn below.
1298             $self->_AddMember(
1299                 PrincipalId             => RT->Nobody->Id,
1300                 RecordTransaction       => 0,
1301                 RecordSetTransaction    => $args{RecordTransaction},
1302             );
1303             $txn{Type}     = "SetWatcher";
1304             $txn{NewValue} = RT->Nobody->id;
1305         } else {
1306             $txn{Type} = "DelWatcher";
1307         }
1308
1309         if ($args{RecordTransaction}) {
1310             my $obj = $args{Object} || $self->RoleGroupObject;
1311             $obj->_NewTransaction(%txn);
1312         }
1313     }
1314
1315     return ( $val, $self->loc("Member deleted") );
1316 }
1317
1318
1319
1320 sub _Set {
1321     my $self = shift;
1322     my %args = (
1323         Field => undef,
1324         Value => undef,
1325         TransactionType   => 'Set',
1326         RecordTransaction => 1,
1327         @_
1328     );
1329
1330     unless ( $self->CurrentUserHasRight('AdminGroup') ) {
1331         return ( 0, $self->loc('Permission Denied') );
1332         }
1333
1334     my $Old = $self->SUPER::_Value("$args{'Field'}");
1335
1336     my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1337                                           Value => $args{'Value'} );
1338
1339     #If we can't actually set the field to the value, don't record
1340     # a transaction. instead, get out of here.
1341     if ( $ret == 0 ) { return ( 0, $msg ); }
1342
1343     if ( $args{'RecordTransaction'} == 1 ) {
1344
1345         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1346                                                Type => $args{'TransactionType'},
1347                                                Field     => $args{'Field'},
1348                                                NewValue  => $args{'Value'},
1349                                                OldValue  => $Old,
1350                                                TimeTaken => $args{'TimeTaken'},
1351         );
1352         return ( $Trans, scalar $TransObj->Description );
1353     }
1354     else {
1355         return ( $ret, $msg );
1356     }
1357 }
1358
1359 =head2 CurrentUserCanSee
1360
1361 Always returns 1; unfortunately, for historical reasons, users have
1362 always been able to examine groups they have indirect access to, even if
1363 they do not have SeeGroup explicitly.
1364
1365 =cut
1366
1367 sub CurrentUserCanSee {
1368     my $self = shift;
1369     return 1;
1370 }
1371
1372
1373 =head2 PrincipalObj
1374
1375 Returns the principal object for this user. returns an empty RT::Principal
1376 if there's no principal object matching this user. 
1377 The response is cached. PrincipalObj should never ever change.
1378
1379
1380 =cut
1381
1382
1383 sub PrincipalObj {
1384     my $self = shift;
1385     my $res = RT::Principal->new( $self->CurrentUser );
1386     $res->Load( $self->id );
1387     return $res;
1388 }
1389
1390
1391 =head2 PrincipalId  
1392
1393 Returns this user's PrincipalId
1394
1395 =cut
1396
1397 sub PrincipalId {
1398     my $self = shift;
1399     return $self->Id;
1400 }
1401
1402 sub InstanceObj {
1403     my $self = shift;
1404
1405     my $class;
1406     if ( $self->Domain eq 'ACLEquivalence' ) {
1407         $class = "RT::User";
1408     } elsif ($self->Domain eq 'RT::Queue-Role') {
1409         $class = "RT::Queue";
1410     } elsif ($self->Domain eq 'RT::Ticket-Role') {
1411         $class = "RT::Ticket";
1412     }
1413
1414     return unless $class;
1415
1416     my $obj = $class->new( $self->CurrentUser );
1417     $obj->Load( $self->Instance );
1418     return $obj;
1419 }
1420
1421 sub BasicColumns {
1422     (
1423         [ Name => 'Name' ],
1424         [ Description => 'Description' ],
1425     );
1426 }
1427
1428
1429 =head1 AUTHOR
1430
1431 Jesse Vincent, jesse@bestpractical.com
1432
1433 =head1 SEE ALSO
1434
1435 RT
1436
1437 =cut
1438
1439
1440
1441
1442
1443 =head2 id
1444
1445 Returns the current value of id.
1446 (In the database, id is stored as int(11).)
1447
1448
1449 =cut
1450
1451
1452 =head2 Name
1453
1454 Returns the current value of Name.
1455 (In the database, Name is stored as varchar(200).)
1456
1457
1458
1459 =head2 SetName VALUE
1460
1461
1462 Set Name to VALUE.
1463 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1464 (In the database, Name will be stored as a varchar(200).)
1465
1466
1467 =cut
1468
1469
1470 =head2 Description
1471
1472 Returns the current value of Description.
1473 (In the database, Description is stored as varchar(255).)
1474
1475
1476
1477 =head2 SetDescription VALUE
1478
1479
1480 Set Description to VALUE.
1481 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1482 (In the database, Description will be stored as a varchar(255).)
1483
1484
1485 =cut
1486
1487
1488 =head2 Domain
1489
1490 Returns the current value of Domain.
1491 (In the database, Domain is stored as varchar(64).)
1492
1493
1494
1495 =head2 SetDomain VALUE
1496
1497
1498 Set Domain to VALUE.
1499 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1500 (In the database, Domain will be stored as a varchar(64).)
1501
1502
1503 =cut
1504
1505
1506 =head2 Type
1507
1508 Returns the current value of Type.
1509 (In the database, Type is stored as varchar(64).)
1510
1511 Deprecated, use Name instead, will be removed in 4.4.
1512
1513 =head2 SetType VALUE
1514
1515
1516 Set Type to VALUE.
1517 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1518 (In the database, Type will be stored as a varchar(64).)
1519
1520 Deprecated, use SetName instead, will be removed in 4.4.
1521
1522 =cut
1523
1524
1525 =head2 Instance
1526
1527 Returns the current value of Instance.
1528 (In the database, Instance is stored as int(11).)
1529
1530
1531
1532 =head2 SetInstance VALUE
1533
1534
1535 Set Instance to VALUE.
1536 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1537 (In the database, Instance will be stored as a int(11).)
1538
1539
1540 =cut
1541
1542
1543 =head2 Creator
1544
1545 Returns the current value of Creator.
1546 (In the database, Creator is stored as int(11).)
1547
1548
1549 =cut
1550
1551
1552 =head2 Created
1553
1554 Returns the current value of Created.
1555 (In the database, Created is stored as datetime.)
1556
1557
1558 =cut
1559
1560
1561 =head2 LastUpdatedBy
1562
1563 Returns the current value of LastUpdatedBy.
1564 (In the database, LastUpdatedBy is stored as int(11).)
1565
1566
1567 =cut
1568
1569
1570 =head2 LastUpdated
1571
1572 Returns the current value of LastUpdated.
1573 (In the database, LastUpdated is stored as datetime.)
1574
1575
1576 =cut
1577
1578
1579
1580 sub _CoreAccessible {
1581     {
1582
1583         id =>
1584                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1585         Name =>
1586                 {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
1587         Description =>
1588                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1589         Domain =>
1590                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1591         Type =>
1592                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1593         Instance =>
1594                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1595         Creator =>
1596                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1597         Created =>
1598                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1599         LastUpdatedBy =>
1600                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1601         LastUpdated =>
1602                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1603
1604  }
1605 };
1606
1607 sub FindDependencies {
1608     my $self = shift;
1609     my ($walker, $deps) = @_;
1610
1611     $self->SUPER::FindDependencies($walker, $deps);
1612
1613     my $instance = $self->InstanceObj;
1614     $deps->Add( out => $instance ) if $instance;
1615
1616     # Group members records, unless we're a system group
1617     if ($self->Domain ne "SystemInternal") {
1618         my $objs = RT::GroupMembers->new( $self->CurrentUser );
1619         $objs->LimitToMembersOfGroup( $self->PrincipalId );
1620         $deps->Add( in => $objs );
1621     }
1622
1623     # Group member records group belongs to
1624     my $objs = RT::GroupMembers->new( $self->CurrentUser );
1625     $objs->Limit( FIELD => 'MemberId', VALUE => $self->PrincipalId );
1626     $deps->Add( in => $objs );
1627 }
1628
1629 sub Serialize {
1630     my $self = shift;
1631     my %args = (@_);
1632     my %store = $self->SUPER::Serialize(@_);
1633
1634     my $instance = $self->InstanceObj;
1635     $store{Instance} = \($instance->UID) if $instance;
1636
1637     $store{Disabled} = $self->PrincipalObj->Disabled;
1638     $store{Principal} = $self->PrincipalObj->UID;
1639     $store{PrincipalId} = $self->PrincipalObj->Id;
1640     return %store;
1641 }
1642
1643 sub PreInflate {
1644     my $class = shift;
1645     my ($importer, $uid, $data) = @_;
1646
1647     my $principal_uid = delete $data->{Principal};
1648     my $principal_id  = delete $data->{PrincipalId};
1649     my $disabled      = delete $data->{Disabled};
1650
1651     # Inflate refs into their IDs
1652     $class->SUPER::PreInflate( $importer, $uid, $data );
1653
1654     # Factored out code, in case we find an existing version of this group
1655     my $obj = RT::Group->new( RT->SystemUser );
1656     my $duplicated = sub {
1657         $importer->SkipTransactions( $uid );
1658         $importer->Resolve(
1659             $principal_uid,
1660             ref($obj->PrincipalObj),
1661             $obj->PrincipalObj->Id
1662         );
1663         $importer->Resolve( $uid => ref($obj), $obj->Id );
1664         return;
1665     };
1666
1667     # Go looking for the pre-existing version of the it
1668     if ($data->{Domain} eq "ACLEquivalence") {
1669         $obj->LoadACLEquivalenceGroup( $data->{Instance} );
1670         return $duplicated->() if $obj->Id;
1671
1672         # Update the name and description for the new ID
1673         $data->{Name} = 'User '. $data->{Instance};
1674         $data->{Description} = 'ACL equiv. for user '.$data->{Instance};
1675     } elsif ($data->{Domain} eq "UserDefined") {
1676         $data->{Name} = $importer->Qualify($data->{Name});
1677         $obj->LoadUserDefinedGroup( $data->{Name} );
1678         if ($obj->Id) {
1679             $importer->MergeValues($obj, $data);
1680             return $duplicated->();
1681         }
1682     } elsif ($data->{Domain} =~ /^(SystemInternal|RT::System-Role)$/) {
1683         $obj->LoadByCols( Domain => $data->{Domain}, Name => $data->{Name} );
1684         return $duplicated->() if $obj->Id;
1685     } elsif ($data->{Domain} eq "RT::Queue-Role") {
1686         my $queue = RT::Queue->new( RT->SystemUser );
1687         $queue->Load( $data->{Instance} );
1688         $obj->LoadRoleGroup( Object => $queue, Name => $data->{Name} );
1689         return $duplicated->() if $obj->Id;
1690     }
1691
1692     my $principal = RT::Principal->new( RT->SystemUser );
1693     my ($id) = $principal->Create(
1694         PrincipalType => 'Group',
1695         Disabled => $disabled,
1696         ObjectId => 0,
1697     );
1698
1699     # Now we have a principal id, set the id for the group record
1700     $data->{id} = $id;
1701
1702     $importer->Resolve( $principal_uid => ref($principal), $id );
1703
1704     $importer->Postpone(
1705         for => $uid,
1706         uid => $principal_uid,
1707         column => "ObjectId",
1708     );
1709
1710     return 1;
1711 }
1712
1713 sub PostInflate {
1714     my $self = shift;
1715
1716     my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
1717     $cgm->Create(
1718         Group  => $self->PrincipalObj,
1719         Member => $self->PrincipalObj,
1720         ImmediateParent => $self->PrincipalObj
1721     );
1722 }
1723
1724 RT::Base->_ImportOverlays();
1725
1726 1;