import rt 3.4.6
[freeside.git] / rt / lib / RT / GroupMember_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
6 #                                          <jesse@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., 675 Mass Ave, Cambridge, MA 02139, USA.
26
27
28 # CONTRIBUTION SUBMISSION POLICY:
29
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
35
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
44
45 # END BPS TAGGED BLOCK }}}
46
47 =head1 NAME
48
49   RT::GroupMember - a member of an RT Group
50
51 =head1 SYNOPSIS
52
53 RT::GroupMember should never be called directly. It should ONLY
54 only be accessed through the helper functions in RT::Group;
55
56 If you're operating on an RT::GroupMember object yourself, you B<ARE>
57 doing something wrong.
58
59 =head1 DESCRIPTION
60
61
62
63
64 =head1 METHODS
65
66
67 =begin testing
68
69 ok (require RT::GroupMember);
70
71 =end testing
72
73
74 =cut
75
76
77 package RT::GroupMember;
78
79 use strict;
80 no warnings qw(redefine);
81 use RT::CachedGroupMembers;
82
83 # {{{ sub Create
84
85 =head2 Create { Group => undef, Member => undef }
86
87 Add a Principal to the group Group.
88 if the Principal is a group, automatically inserts all
89 members of the principal into the cached members table recursively down.
90
91 Both Group and Member are expected to be RT::Principal objects
92
93 =cut
94
95 sub Create {
96     my $self = shift;
97     my %args = (
98         Group  => undef,
99         Member => undef,
100         InsideTransaction => undef,
101         @_
102     );
103
104     unless ($args{'Group'} &&
105             UNIVERSAL::isa($args{'Group'}, 'RT::Principal') &&
106             $args{'Group'}->Id ) {
107
108         $RT::Logger->warning("GroupMember::Create called with a bogus Group arg");
109         return (undef);
110     }
111
112     unless($args{'Group'}->IsGroup) {
113         $RT::Logger->warning("Someone tried to add a member to a user instead of a group");
114         return (undef);
115     }
116
117     unless ($args{'Member'} && 
118             UNIVERSAL::isa($args{'Member'}, 'RT::Principal') &&
119             $args{'Member'}->Id) {
120         $RT::Logger->warning("GroupMember::Create called with a bogus Principal arg");
121         return (undef);
122     }
123
124
125     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
126     # TODO what about the groups key cache?
127     RT::Principal->InvalidateACLCache();
128
129     $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
130
131     # We really need to make sure we don't add any members to this group
132     # that contain the group itself. that would, um, suck. 
133     # (and recurse infinitely)  Later, we can add code to check this in the 
134     # cache and bail so we can support cycling directed graphs
135
136     if ($args{'Member'}->IsGroup) {
137         my $member_object = $args{'Member'}->Object;
138         if ($member_object->HasMemberRecursively($args{'Group'})) {
139             $RT::Logger->debug("Adding that group would create a loop");
140             return(undef);
141         }
142         elsif ( $args{'Member'}->Id == $args{'Group'}->Id) {
143             $RT::Logger->debug("Can't add a group to itself");
144             return(undef);
145         }
146     }
147
148
149     my $id = $self->SUPER::Create(
150         GroupId  => $args{'Group'}->Id,
151         MemberId => $args{'Member'}->Id
152     );
153
154     unless ($id) {
155         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
156         return (undef);
157     }
158
159     my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser );
160     my $cached_id     = $cached_member->Create(
161         Member          => $args{'Member'},
162         Group           => $args{'Group'},
163         ImmediateParent => $args{'Group'},
164         Via             => '0'
165     );
166
167
168     #When adding a member to a group, we need to go back
169     #and popuplate the CachedGroupMembers of all the groups that group is part of .
170
171     my $cgm = RT::CachedGroupMembers->new( $self->CurrentUser );
172
173     # find things which have the current group as a member. 
174     # $group is an RT::Principal for the group.
175     $cgm->LimitToGroupsWithMember( $args{'Group'}->Id );
176
177     while ( my $parent_member = $cgm->Next ) {
178         my $parent_id = $parent_member->MemberId;
179         my $via       = $parent_member->Id;
180         my $group_id  = $parent_member->GroupId;
181
182           my $other_cached_member =
183           RT::CachedGroupMember->new( $self->CurrentUser );
184         my $other_cached_id = $other_cached_member->Create(
185             Member          => $args{'Member'},
186                       Group => $parent_member->GroupObj,
187             ImmediateParent => $parent_member->MemberObj,
188             Via             => $parent_member->Id
189         );
190         unless ($other_cached_id) {
191             $RT::Logger->err( "Couldn't add " . $args{'Member'}
192                   . " as a submember of a supergroup" );
193             $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
194             return (undef);
195         }
196     } 
197
198     unless ($cached_id) {
199         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
200         return (undef);
201     }
202
203     $RT::Handle->Commit() unless ($args{'InsideTransaction'});
204
205     return ($id);
206 }
207
208 # }}}
209
210 # {{{ sub _StashUser
211
212 =head2 _StashUser PRINCIPAL
213
214 Create { Group => undef, Member => undef }
215
216 Creates an entry in the groupmembers table, which lists a user
217 as a member of himself. This makes ACL checks a whole bunch easier.
218 This happens once on user create and never ever gets yanked out.
219
220 PRINCIPAL is expected to be an RT::Principal object for a user
221
222 This routine expects to be called inside a transaction by RT::User->Create
223
224 =cut
225
226 sub _StashUser {
227     my $self = shift;
228     my %args = (
229         Group  => undef,
230         Member => undef,
231         @_
232     );
233
234     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
235     # TODO what about the groups key cache?
236     RT::Principal->InvalidateACLCache();
237
238
239     # We really need to make sure we don't add any members to this group
240     # that contain the group itself. that would, um, suck. 
241     # (and recurse infinitely)  Later, we can add code to check this in the 
242     # cache and bail so we can support cycling directed graphs
243
244     my $id = $self->SUPER::Create(
245         GroupId  => $args{'Group'}->Id,
246         MemberId => $args{'Member'}->Id,
247     );
248
249     unless ($id) {
250         return (undef);
251     }
252
253     my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser );
254     my $cached_id     = $cached_member->Create(
255         Member          => $args{'Member'},
256         Group           => $args{'Group'},
257         ImmediateParent => $args{'Group'},
258         Via             => '0'
259     );
260
261     unless ($cached_id) {
262         return (undef);
263     }
264
265     return ($id);
266 }
267
268 # }}}
269
270 # {{{ sub Delete
271
272 =head2 Delete
273
274 Takes no arguments. deletes the currently loaded member from the 
275 group in question.
276
277 Expects to be called _outside_ a transaction
278
279 =cut
280
281 sub Delete {
282     my $self = shift;
283
284
285     $RT::Handle->BeginTransaction();
286
287     # Find all occurrences of this member as a member of this group
288     # in the cache and nuke them, recursively.
289
290     # The following code will delete all Cached Group members
291     # where this member's group is _not_ the primary group 
292     # (Ie if we're deleting C as a member of B, and B happens to be 
293     # a member of A, will delete C as a member of A without touching
294     # C as a member of B
295
296     my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
297
298     $cached_submembers->Limit(
299         FIELD    => 'MemberId',
300         OPERATOR => '=',
301         VALUE    => $self->MemberObj->Id
302     );
303
304     $cached_submembers->Limit(
305         FIELD    => 'ImmediateParentId',
306         OPERATOR => '=',
307         VALUE    => $self->GroupObj->Id
308     );
309
310
311
312
313
314     while ( my $item_to_del = $cached_submembers->Next() ) {
315         my $del_err = $item_to_del->Delete();
316         unless ($del_err) {
317             $RT::Handle->Rollback();
318             $RT::Logger->warning("Couldn't delete cached group submember ".$item_to_del->Id);
319             return (undef);
320         }
321     }
322
323     my ($err, $msg) = $self->SUPER::Delete();
324     unless ($err) {
325             $RT::Logger->warning("Couldn't delete cached group submember ".$self->Id);
326         $RT::Handle->Rollback();
327         return (undef);
328     }
329
330     # Since this deletion may have changed the former member's
331     # delegation rights, we need to ensure that no invalid delegations
332     # remain.
333     $err = $self->MemberObj->_CleanupInvalidDelegations(InsideTransaction => 1);
334     unless ($err) {
335         $RT::Logger->warning("Unable to revoke delegated rights for principal ".$self->Id);
336         $RT::Handle->Rollback();
337         return (undef);
338     }
339
340     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
341     # TODO what about the groups key cache?
342     RT::Principal->InvalidateACLCache();
343
344     $RT::Handle->Commit();
345     return ($err);
346
347 }
348
349 # }}}
350
351 # {{{ sub MemberObj
352
353 =head2 MemberObj
354
355 Returns an RT::Principal object for the Principal specified by $self->PrincipalId
356
357 =cut
358
359 sub MemberObj {
360     my $self = shift;
361     unless ( defined( $self->{'Member_obj'} ) ) {
362         $self->{'Member_obj'} = RT::Principal->new( $self->CurrentUser );
363         $self->{'Member_obj'}->Load( $self->MemberId ) if ($self->MemberId);
364     }
365     return ( $self->{'Member_obj'} );
366 }
367
368 # }}}
369
370 # {{{ sub GroupObj
371
372 =head2 GroupObj
373
374 Returns an RT::Principal object for the Group specified in $self->GroupId
375
376 =cut
377
378 sub GroupObj {
379     my $self = shift;
380     unless ( defined( $self->{'Group_obj'} ) ) {
381         $self->{'Group_obj'} = RT::Principal->new( $self->CurrentUser );
382         $self->{'Group_obj'}->Load( $self->GroupId );
383     }
384     return ( $self->{'Group_obj'} );
385 }
386
387 # }}}
388
389 1;