1d4090cc6c945ea145f2f5de17641313ff3af696
[freeside.git] / rt / lib / RT / GroupMember.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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 Create {
99     my $self = shift;
100     my %args = (
101         Group  => undef,
102         Member => undef,
103         InsideTransaction => undef,
104         @_
105     );
106
107     unless ($args{'Group'} &&
108             UNIVERSAL::isa($args{'Group'}, 'RT::Principal') &&
109             $args{'Group'}->Id ) {
110
111         $RT::Logger->warning("GroupMember::Create called with a bogus Group arg");
112         return (undef);
113     }
114
115     unless($args{'Group'}->IsGroup) {
116         $RT::Logger->warning("Someone tried to add a member to a user instead of a group");
117         return (undef);
118     }
119
120     unless ($args{'Member'} && 
121             UNIVERSAL::isa($args{'Member'}, 'RT::Principal') &&
122             $args{'Member'}->Id) {
123         $RT::Logger->warning("GroupMember::Create called with a bogus Principal arg");
124         return (undef);
125     }
126
127
128     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
129     # TODO what about the groups key cache?
130     RT::Principal->InvalidateACLCache();
131
132     $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
133
134     # We really need to make sure we don't add any members to this group
135     # that contain the group itself. that would, um, suck. 
136     # (and recurse infinitely)  Later, we can add code to check this in the 
137     # cache and bail so we can support cycling directed graphs
138
139     if ($args{'Member'}->IsGroup) {
140         my $member_object = $args{'Member'}->Object;
141         if ($member_object->HasMemberRecursively($args{'Group'})) {
142             $RT::Logger->debug("Adding that group would create a loop");
143             $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
144             return(undef);
145         }
146         elsif ( $args{'Member'}->Id == $args{'Group'}->Id) {
147             $RT::Logger->debug("Can't add a group to itself");
148             $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
149             return(undef);
150         }
151     }
152
153
154     my $id = $self->SUPER::Create(
155         GroupId  => $args{'Group'}->Id,
156         MemberId => $args{'Member'}->Id
157     );
158
159     unless ($id) {
160         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
161         return (undef);
162     }
163
164     my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser );
165     my $cached_id     = $cached_member->Create(
166         Member          => $args{'Member'},
167         Group           => $args{'Group'},
168         ImmediateParent => $args{'Group'},
169         Via             => '0'
170     );
171
172
173     #When adding a member to a group, we need to go back
174     #and popuplate the CachedGroupMembers of all the groups that group is part of .
175
176     my $cgm = RT::CachedGroupMembers->new( $self->CurrentUser );
177
178     # find things which have the current group as a member. 
179     # $group is an RT::Principal for the group.
180     $cgm->LimitToGroupsWithMember( $args{'Group'}->Id );
181     $cgm->Limit(
182         SUBCLAUSE => 'filter', # dont't mess up with prev condition
183         FIELD => 'MemberId',
184         OPERATOR => '!=',
185         VALUE => 'main.GroupId',
186         QUOTEVALUE => 0,
187         ENTRYAGGREGATOR => 'AND',
188     );
189
190     while ( my $parent_member = $cgm->Next ) {
191         my $parent_id = $parent_member->MemberId;
192         my $via       = $parent_member->Id;
193         my $group_id  = $parent_member->GroupId;
194
195           my $other_cached_member =
196           RT::CachedGroupMember->new( $self->CurrentUser );
197         my $other_cached_id = $other_cached_member->Create(
198             Member          => $args{'Member'},
199                       Group => $parent_member->GroupObj,
200             ImmediateParent => $parent_member->MemberObj,
201             Via             => $parent_member->Id
202         );
203         unless ($other_cached_id) {
204             $RT::Logger->err( "Couldn't add " . $args{'Member'}
205                   . " as a submember of a supergroup" );
206             $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
207             return (undef);
208         }
209     } 
210
211     unless ($cached_id) {
212         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
213         return (undef);
214     }
215
216     $RT::Handle->Commit() unless ($args{'InsideTransaction'});
217
218     return ($id);
219 }
220
221
222
223 =head2 _StashUser PRINCIPAL
224
225 Create { Group => undef, Member => undef }
226
227 Creates an entry in the groupmembers table, which lists a user
228 as a member of himself. This makes ACL checks a whole bunch easier.
229 This happens once on user create and never ever gets yanked out.
230
231 PRINCIPAL is expected to be an RT::Principal object for a user
232
233 This routine expects to be called inside a transaction by RT::User->Create
234
235 =cut
236
237 sub _StashUser {
238     my $self = shift;
239     my %args = (
240         Group  => undef,
241         Member => undef,
242         @_
243     );
244
245     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
246     # TODO what about the groups key cache?
247     RT::Principal->InvalidateACLCache();
248
249
250     # We really need to make sure we don't add any members to this group
251     # that contain the group itself. that would, um, suck. 
252     # (and recurse infinitely)  Later, we can add code to check this in the 
253     # cache and bail so we can support cycling directed graphs
254
255     my $id = $self->SUPER::Create(
256         GroupId  => $args{'Group'}->Id,
257         MemberId => $args{'Member'}->Id,
258     );
259
260     unless ($id) {
261         return (undef);
262     }
263
264     my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser );
265     my $cached_id     = $cached_member->Create(
266         Member          => $args{'Member'},
267         Group           => $args{'Group'},
268         ImmediateParent => $args{'Group'},
269         Via             => '0'
270     );
271
272     unless ($cached_id) {
273         return (undef);
274     }
275
276     return ($id);
277 }
278
279
280
281 =head2 Delete
282
283 Takes no arguments. deletes the currently loaded member from the 
284 group in question.
285
286 Expects to be called _outside_ a transaction
287
288 =cut
289
290 sub Delete {
291     my $self = shift;
292
293
294     $RT::Handle->BeginTransaction();
295
296     # Find all occurrences of this member as a member of this group
297     # in the cache and nuke them, recursively.
298
299     # The following code will delete all Cached Group members
300     # where this member's group is _not_ the primary group 
301     # (Ie if we're deleting C as a member of B, and B happens to be 
302     # a member of A, will delete C as a member of A without touching
303     # C as a member of B
304
305     my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
306
307     $cached_submembers->Limit(
308         FIELD    => 'MemberId',
309         OPERATOR => '=',
310         VALUE    => $self->MemberObj->Id
311     );
312
313     $cached_submembers->Limit(
314         FIELD    => 'ImmediateParentId',
315         OPERATOR => '=',
316         VALUE    => $self->GroupObj->Id
317     );
318
319
320
321
322
323     while ( my $item_to_del = $cached_submembers->Next() ) {
324         my $del_err = $item_to_del->Delete();
325         unless ($del_err) {
326             $RT::Handle->Rollback();
327             $RT::Logger->warning("Couldn't delete cached group submember ".$item_to_del->Id);
328             return (undef);
329         }
330     }
331
332     my ($err, $msg) = $self->SUPER::Delete();
333     unless ($err) {
334             $RT::Logger->warning("Couldn't delete cached group submember ".$self->Id);
335         $RT::Handle->Rollback();
336         return (undef);
337     }
338
339     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
340     # TODO what about the groups key cache?
341     RT::Principal->InvalidateACLCache();
342
343     $RT::Handle->Commit();
344     return ($err);
345
346 }
347
348
349
350 =head2 MemberObj
351
352 Returns an RT::Principal object for the Principal specified by $self->MemberId
353
354 =cut
355
356 sub MemberObj {
357     my $self = shift;
358     unless ( defined( $self->{'Member_obj'} ) ) {
359         $self->{'Member_obj'} = RT::Principal->new( $self->CurrentUser );
360         $self->{'Member_obj'}->Load( $self->MemberId ) if ($self->MemberId);
361     }
362     return ( $self->{'Member_obj'} );
363 }
364
365
366
367 =head2 GroupObj
368
369 Returns an RT::Principal object for the Group specified in $self->GroupId
370
371 =cut
372
373 sub GroupObj {
374     my $self = shift;
375     unless ( defined( $self->{'Group_obj'} ) ) {
376         $self->{'Group_obj'} = RT::Principal->new( $self->CurrentUser );
377         $self->{'Group_obj'}->Load( $self->GroupId );
378     }
379     return ( $self->{'Group_obj'} );
380 }
381
382
383
384
385
386
387 =head2 id
388
389 Returns the current value of id.
390 (In the database, id is stored as int(11).)
391
392
393 =cut
394
395
396 =head2 GroupId
397
398 Returns the current value of GroupId.
399 (In the database, GroupId is stored as int(11).)
400
401
402
403 =head2 SetGroupId VALUE
404
405
406 Set GroupId to VALUE.
407 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
408 (In the database, GroupId will be stored as a int(11).)
409
410
411 =cut
412
413
414 =head2 MemberId
415
416 Returns the current value of MemberId.
417 (In the database, MemberId is stored as int(11).)
418
419
420
421 =head2 SetMemberId VALUE
422
423
424 Set MemberId to VALUE.
425 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
426 (In the database, MemberId will be stored as a int(11).)
427
428
429 =cut
430
431
432 =head2 Creator
433
434 Returns the current value of Creator.
435 (In the database, Creator is stored as int(11).)
436
437
438 =cut
439
440
441 =head2 Created
442
443 Returns the current value of Created.
444 (In the database, Created is stored as datetime.)
445
446
447 =cut
448
449
450 =head2 LastUpdatedBy
451
452 Returns the current value of LastUpdatedBy.
453 (In the database, LastUpdatedBy is stored as int(11).)
454
455
456 =cut
457
458
459 =head2 LastUpdated
460
461 Returns the current value of LastUpdated.
462 (In the database, LastUpdated is stored as datetime.)
463
464
465 =cut
466
467
468
469 sub _CoreAccessible {
470     {
471
472         id =>
473                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
474         GroupId =>
475                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
476         MemberId =>
477                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
478         Creator =>
479                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
480         Created =>
481                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
482         LastUpdatedBy =>
483                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
484         LastUpdated =>
485                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
486
487  }
488 };
489
490 RT::Base->_ImportOverlays();
491
492 1;