4de4592e0467952f9427790fe44b6b8a6b35c5c1
[freeside.git] / rt / lib / RT / GroupMember.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::GroupMember - a member of an RT Group
52
53 =head1 SYNOPSIS
54
55 RT::GroupMember should never be called directly. It should ONLY
56 only be accessed through the helper functions in RT::Group;
57
58 If you're operating on an RT::GroupMember object yourself, you B<ARE>
59 doing something wrong.
60
61 =head1 DESCRIPTION
62
63
64
65
66 =head1 METHODS
67
68
69
70
71 =cut
72
73
74 package RT::GroupMember;
75
76 use strict;
77 use warnings;
78
79
80 use base 'RT::Record';
81
82 sub Table {'GroupMembers'}
83
84
85 use RT::CachedGroupMembers;
86
87
88 =head2 Create { Group => undef, Member => undef }
89
90 Add a Principal to the group Group.
91 if the Principal is a group, automatically inserts all
92 members of the principal into the cached members table recursively down.
93
94 Both Group and Member are expected to be RT::Principal objects
95
96 =cut
97
98 sub _InsertCGM {
99     my $self = shift;
100
101     my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser );
102     my $cached_id     = $cached_member->Create(
103         Member          => $self->MemberObj,
104         Group           => $self->GroupObj,
105         ImmediateParent => $self->GroupObj,
106         Via             => '0'
107     );
108
109
110     #When adding a member to a group, we need to go back
111     #and popuplate the CachedGroupMembers of all the groups that group is part of .
112
113     my $cgm = RT::CachedGroupMembers->new( $self->CurrentUser );
114
115     # find things which have the current group as a member. 
116     # $group is an RT::Principal for the group.
117     $cgm->LimitToGroupsWithMember( $self->GroupId );
118     $cgm->Limit(
119         SUBCLAUSE => 'filter', # dont't mess up with prev condition
120         FIELD => 'MemberId',
121         OPERATOR => '!=',
122         VALUE => 'main.GroupId',
123         QUOTEVALUE => 0,
124         ENTRYAGGREGATOR => 'AND',
125     );
126
127     while ( my $parent_member = $cgm->Next ) {
128         my $parent_id = $parent_member->MemberId;
129         my $via       = $parent_member->Id;
130         my $group_id  = $parent_member->GroupId;
131
132         my $other_cached_member =
133             RT::CachedGroupMember->new( $self->CurrentUser );
134         my $other_cached_id = $other_cached_member->Create(
135             Member          => $self->MemberObj,
136                       Group => $parent_member->GroupObj,
137             ImmediateParent => $parent_member->MemberObj,
138             Via             => $parent_member->Id
139         );
140         unless ($other_cached_id) {
141             $RT::Logger->err( "Couldn't add " . $self->MemberId
142                   . " as a submember of a supergroup" );
143             return;
144         }
145     }
146
147     return $cached_id;
148 }
149
150 sub Create {
151     my $self = shift;
152     my %args = (
153         Group  => undef,
154         Member => undef,
155         InsideTransaction => undef,
156         @_
157     );
158
159     unless ($args{'Group'} &&
160             UNIVERSAL::isa($args{'Group'}, 'RT::Principal') &&
161             $args{'Group'}->Id ) {
162
163         $RT::Logger->warning("GroupMember::Create called with a bogus Group arg");
164         return (undef);
165     }
166
167     unless($args{'Group'}->IsGroup) {
168         $RT::Logger->warning("Someone tried to add a member to a user instead of a group");
169         return (undef);
170     }
171
172     unless ($args{'Member'} && 
173             UNIVERSAL::isa($args{'Member'}, 'RT::Principal') &&
174             $args{'Member'}->Id) {
175         $RT::Logger->warning("GroupMember::Create called with a bogus Principal arg");
176         return (undef);
177     }
178
179
180     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
181     # TODO what about the groups key cache?
182     RT::Principal->InvalidateACLCache();
183
184     $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
185
186     # We really need to make sure we don't add any members to this group
187     # that contain the group itself. that would, um, suck. 
188     # (and recurse infinitely)  Later, we can add code to check this in the 
189     # cache and bail so we can support cycling directed graphs
190
191     if ($args{'Member'}->IsGroup) {
192         my $member_object = $args{'Member'}->Object;
193         if ($member_object->HasMemberRecursively($args{'Group'})) {
194             $RT::Logger->debug("Adding that group would create a loop");
195             $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
196             return(undef);
197         }
198         elsif ( $args{'Member'}->Id == $args{'Group'}->Id) {
199             $RT::Logger->debug("Can't add a group to itself");
200             $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
201             return(undef);
202         }
203     }
204
205
206     my $id = $self->SUPER::Create(
207         GroupId  => $args{'Group'}->Id,
208         MemberId => $args{'Member'}->Id
209     );
210
211     unless ($id) {
212         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
213         return (undef);
214     }
215
216     my $clone = RT::GroupMember->new( $self->CurrentUser );
217     $clone->Load( $id );
218     my $cached_id = $clone->_InsertCGM;
219
220     unless ($cached_id) {
221         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
222         return (undef);
223     }
224
225     $RT::Handle->Commit() unless ($args{'InsideTransaction'});
226
227     return ($id);
228 }
229
230
231
232 =head2 _StashUser PRINCIPAL
233
234 Create { Group => undef, Member => undef }
235
236 Creates an entry in the groupmembers table, which lists a user
237 as a member of himself. This makes ACL checks a whole bunch easier.
238 This happens once on user create and never ever gets yanked out.
239
240 PRINCIPAL is expected to be an RT::Principal object for a user
241
242 This routine expects to be called inside a transaction by RT::User->Create
243
244 =cut
245
246 sub _StashUser {
247     my $self = shift;
248     my %args = (
249         Group  => undef,
250         Member => undef,
251         @_
252     );
253
254     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
255     # TODO what about the groups key cache?
256     RT::Principal->InvalidateACLCache();
257
258
259     # We really need to make sure we don't add any members to this group
260     # that contain the group itself. that would, um, suck. 
261     # (and recurse infinitely)  Later, we can add code to check this in the 
262     # cache and bail so we can support cycling directed graphs
263
264     my $id = $self->SUPER::Create(
265         GroupId  => $args{'Group'}->Id,
266         MemberId => $args{'Member'}->Id,
267     );
268
269     unless ($id) {
270         return (undef);
271     }
272
273     my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser );
274     my $cached_id     = $cached_member->Create(
275         Member          => $args{'Member'},
276         Group           => $args{'Group'},
277         ImmediateParent => $args{'Group'},
278         Via             => '0'
279     );
280
281     unless ($cached_id) {
282         return (undef);
283     }
284
285     return ($id);
286 }
287
288
289
290 =head2 Delete
291
292 Takes no arguments. deletes the currently loaded member from the 
293 group in question.
294
295 Expects to be called _outside_ a transaction
296
297 =cut
298
299 sub Delete {
300     my $self = shift;
301
302
303     $RT::Handle->BeginTransaction();
304
305     # Find all occurrences of this member as a member of this group
306     # in the cache and nuke them, recursively.
307
308     # The following code will delete all Cached Group members
309     # where this member's group is _not_ the primary group 
310     # (Ie if we're deleting C as a member of B, and B happens to be 
311     # a member of A, will delete C as a member of A without touching
312     # C as a member of B
313
314     my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
315
316     $cached_submembers->Limit(
317         FIELD    => 'MemberId',
318         OPERATOR => '=',
319         VALUE    => $self->MemberObj->Id
320     );
321
322     $cached_submembers->Limit(
323         FIELD    => 'ImmediateParentId',
324         OPERATOR => '=',
325         VALUE    => $self->GroupObj->Id
326     );
327
328
329
330
331
332     while ( my $item_to_del = $cached_submembers->Next() ) {
333         my $del_err = $item_to_del->Delete();
334         unless ($del_err) {
335             $RT::Handle->Rollback();
336             $RT::Logger->warning("Couldn't delete cached group submember ".$item_to_del->Id);
337             return (undef);
338         }
339     }
340
341     my ($err, $msg) = $self->SUPER::Delete();
342     unless ($err) {
343             $RT::Logger->warning("Couldn't delete cached group submember ".$self->Id);
344         $RT::Handle->Rollback();
345         return (undef);
346     }
347
348     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
349     # TODO what about the groups key cache?
350     RT::Principal->InvalidateACLCache();
351
352     $RT::Handle->Commit();
353     return ($err);
354
355 }
356
357
358
359 =head2 MemberObj
360
361 Returns an RT::Principal object for the Principal specified by $self->MemberId
362
363 =cut
364
365 sub MemberObj {
366     my $self = shift;
367     unless ( defined( $self->{'Member_obj'} ) ) {
368         $self->{'Member_obj'} = RT::Principal->new( $self->CurrentUser );
369         $self->{'Member_obj'}->Load( $self->MemberId ) if ($self->MemberId);
370     }
371     return ( $self->{'Member_obj'} );
372 }
373
374
375
376 =head2 GroupObj
377
378 Returns an RT::Principal object for the Group specified in $self->GroupId
379
380 =cut
381
382 sub GroupObj {
383     my $self = shift;
384     unless ( defined( $self->{'Group_obj'} ) ) {
385         $self->{'Group_obj'} = RT::Principal->new( $self->CurrentUser );
386         $self->{'Group_obj'}->Load( $self->GroupId );
387     }
388     return ( $self->{'Group_obj'} );
389 }
390
391
392
393
394
395
396 =head2 id
397
398 Returns the current value of id.
399 (In the database, id is stored as int(11).)
400
401
402 =cut
403
404
405 =head2 GroupId
406
407 Returns the current value of GroupId.
408 (In the database, GroupId is stored as int(11).)
409
410
411
412 =head2 SetGroupId VALUE
413
414
415 Set GroupId to VALUE.
416 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
417 (In the database, GroupId will be stored as a int(11).)
418
419
420 =cut
421
422
423 =head2 MemberId
424
425 Returns the current value of MemberId.
426 (In the database, MemberId is stored as int(11).)
427
428
429
430 =head2 SetMemberId VALUE
431
432
433 Set MemberId to VALUE.
434 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
435 (In the database, MemberId will be stored as a int(11).)
436
437
438 =cut
439
440
441 =head2 Creator
442
443 Returns the current value of Creator.
444 (In the database, Creator is stored as int(11).)
445
446
447 =cut
448
449
450 =head2 Created
451
452 Returns the current value of Created.
453 (In the database, Created is stored as datetime.)
454
455
456 =cut
457
458
459 =head2 LastUpdatedBy
460
461 Returns the current value of LastUpdatedBy.
462 (In the database, LastUpdatedBy is stored as int(11).)
463
464
465 =cut
466
467
468 =head2 LastUpdated
469
470 Returns the current value of LastUpdated.
471 (In the database, LastUpdated is stored as datetime.)
472
473
474 =cut
475
476
477
478 sub _CoreAccessible {
479     {
480
481         id =>
482                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
483         GroupId =>
484                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
485         MemberId =>
486                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
487         Creator =>
488                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
489         Created =>
490                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
491         LastUpdatedBy =>
492                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
493         LastUpdated =>
494                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
495
496  }
497 };
498
499 sub FindDependencies {
500     my $self = shift;
501     my ($walker, $deps) = @_;
502
503     $self->SUPER::FindDependencies($walker, $deps);
504
505     $deps->Add( out => $self->GroupObj->Object );
506     $deps->Add( out => $self->MemberObj->Object );
507 }
508
509 sub __DependsOn {
510     my $self = shift;
511     my %args = (
512         Shredder => undef,
513         Dependencies => undef,
514         @_,
515     );
516     my $deps = $args{'Dependencies'};
517     my $list = [];
518
519     my $objs = RT::CachedGroupMembers->new( $self->CurrentUser );
520     $objs->Limit( FIELD => 'MemberId', VALUE => $self->MemberId );
521     $objs->Limit( FIELD => 'ImmediateParentId', VALUE => $self->GroupId );
522     push( @$list, $objs );
523
524     $deps->_PushDependencies(
525         BaseObject => $self,
526         Flags => RT::Shredder::Constants::DEPENDS_ON,
527         TargetObjects => $list,
528         Shredder => $args{'Shredder'}
529     );
530
531     my $group = $self->GroupObj->Object;
532     # XXX: If we delete member of the ticket owner role group then we should also
533     # fix ticket object, but only if we don't plan to delete group itself!
534     unless( ($group->Name || '') eq 'Owner' &&
535         ($group->Domain || '') eq 'RT::Ticket-Role' ) {
536         return $self->SUPER::__DependsOn( %args );
537     }
538
539     # we don't delete group, so we have to fix Ticket and Group
540     $deps->_PushDependencies(
541         BaseObject => $self,
542         Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::VARIABLE,
543         TargetObjects => $group,
544         Shredder => $args{'Shredder'}
545     );
546     $args{'Shredder'}->PutResolver(
547         BaseClass => ref $self,
548         TargetClass => ref $group,
549         Code => sub {
550             my %args = (@_);
551             my $group = $args{'TargetObject'};
552             return if $args{'Shredder'}->GetState( Object => $group )
553                 & (RT::Shredder::Constants::WIPED|RT::Shredder::Constants::IN_WIPING);
554             return unless ($group->Name || '') eq 'Owner';
555             return unless ($group->Domain || '') eq 'RT::Ticket-Role';
556
557             return if $group->MembersObj->Count > 1;
558
559             my $group_member = $args{'BaseObject'};
560
561             if( $group_member->MemberObj->id == RT->Nobody->id ) {
562                 RT::Shredder::Exception->throw( "Couldn't delete Nobody from owners role group" );
563             }
564
565             my( $status, $msg ) = $group->AddMember( RT->Nobody->id );
566
567             RT::Shredder::Exception->throw( $msg ) unless $status;
568
569             return;
570         },
571     );
572
573     return $self->SUPER::__DependsOn( %args );
574 }
575
576 sub PreInflate {
577     my $class = shift;
578     my ($importer, $uid, $data) = @_;
579
580     $class->SUPER::PreInflate( $importer, $uid, $data );
581
582     my $obj = RT::GroupMember->new( RT->SystemUser );
583     $obj->LoadByCols(
584         GroupId  => $data->{GroupId},
585         MemberId => $data->{MemberId},
586     );
587     if ($obj->id) {
588         $importer->Resolve( $uid => ref($obj) => $obj->Id );
589         return;
590     }
591
592     return 1;
593 }
594
595 sub PostInflate {
596     my $self = shift;
597
598     $self->_InsertCGM;
599 }
600
601 RT::Base->_ImportOverlays();
602
603 1;