]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | |
2 | # BEGIN BPS TAGGED BLOCK {{{ | |
3 | # | |
4 | # COPYRIGHT: | |
5 | # | |
403d7b0b | 6 | # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC |
84fb5b46 MKG |
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 | ||
403d7b0b | 54 | RT::Group - RT's group object |
84fb5b46 MKG |
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 | ||
84fb5b46 MKG |
65 | =cut |
66 | ||
67 | ||
68 | package RT::Group; | |
69 | ||
70 | ||
71 | use strict; | |
72 | use warnings; | |
73 | ||
74 | use base 'RT::Record'; | |
75 | ||
af59614d MKG |
76 | use Role::Basic 'with'; |
77 | with "RT::Record::Role::Rights"; | |
78 | ||
84fb5b46 MKG |
79 | sub Table {'Groups'} |
80 | ||
81 | ||
82 | ||
83 | use RT::Users; | |
84 | use RT::GroupMembers; | |
85 | use RT::Principals; | |
86 | use RT::ACL; | |
87 | ||
af59614d MKG |
88 | __PACKAGE__->AddRight( Admin => AdminGroup => 'Modify group metadata or delete group'); # loc_pair |
89 | __PACKAGE__->AddRight( Admin => AdminGroupMembership => 'Modify group membership roster'); # loc_pair | |
90 | __PACKAGE__->AddRight( Staff => ModifyOwnMembership => 'Join or leave group'); # loc_pair | |
91 | __PACKAGE__->AddRight( Admin => EditSavedSearches => 'Create, modify and delete saved searches'); # loc_pair | |
92 | __PACKAGE__->AddRight( Staff => ShowSavedSearches => 'View saved searches'); # loc_pair | |
93 | __PACKAGE__->AddRight( Staff => SeeGroup => 'View group'); # loc_pair | |
94 | __PACKAGE__->AddRight( Staff => SeeGroupDashboard => 'View group dashboards'); # loc_pair | |
95 | __PACKAGE__->AddRight( Admin => CreateGroupDashboard => 'Create group dashboards'); # loc_pair | |
96 | __PACKAGE__->AddRight( Admin => ModifyGroupDashboard => 'Modify group dashboards'); # loc_pair | |
97 | __PACKAGE__->AddRight( Admin => DeleteGroupDashboard => 'Delete group dashboards'); # loc_pair | |
84fb5b46 | 98 | |
af59614d | 99 | =head1 METHODS |
84fb5b46 MKG |
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 { | |
af59614d MKG |
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 | } | |
84fb5b46 MKG |
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', | |
af59614d | 211 | Name => 'UserEquiv', |
84fb5b46 MKG |
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', | |
af59614d | 233 | Name => $identifier, |
84fb5b46 MKG |
234 | ); |
235 | } | |
236 | ||
af59614d | 237 | =head2 LoadRoleGroup |
84fb5b46 | 238 | |
af59614d MKG |
239 | Takes a paramhash of Object and Name and attempts to load the suitable role |
240 | group for said object. | |
84fb5b46 | 241 | |
af59614d | 242 | =cut |
84fb5b46 | 243 | |
af59614d MKG |
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}; | |
84fb5b46 | 253 | |
af59614d MKG |
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 | } | |
84fb5b46 | 263 | |
af59614d MKG |
264 | |
265 | =head2 LoadTicketRoleGroup { Ticket => TICKET_ID, Name => TYPE } | |
266 | ||
267 | Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>. | |
84fb5b46 MKG |
268 | |
269 | =cut | |
270 | ||
271 | sub LoadTicketRoleGroup { | |
af59614d MKG |
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 | ); | |
84fb5b46 MKG |
288 | } |
289 | ||
290 | ||
291 | ||
292 | =head2 LoadQueueRoleGroup { Queue => Queue_ID, Type => TYPE } | |
293 | ||
af59614d | 294 | Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>. |
84fb5b46 MKG |
295 | |
296 | =cut | |
297 | ||
298 | sub LoadQueueRoleGroup { | |
af59614d MKG |
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 | ); | |
84fb5b46 MKG |
315 | } |
316 | ||
317 | ||
318 | ||
af59614d | 319 | =head2 LoadSystemRoleGroup Name |
84fb5b46 | 320 | |
af59614d | 321 | Deprecated in favor of L</LoadRoleGroup> or L<RT::Record/RoleGroup>. |
84fb5b46 MKG |
322 | |
323 | =cut | |
324 | ||
325 | sub LoadSystemRoleGroup { | |
af59614d | 326 | my $self = shift; |
84fb5b46 | 327 | my $type = shift; |
af59614d MKG |
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 ); | |
84fb5b46 MKG |
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, | |
84fb5b46 MKG |
380 | Instance => '0', |
381 | InsideTransaction => undef, | |
382 | _RecordTransaction => 1, | |
383 | @_ | |
384 | ); | |
af59614d MKG |
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 | } | |
84fb5b46 MKG |
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 | ||
af59614d | 468 | return($self->_Create( Domain => 'UserDefined', Instance => '', @_)); |
84fb5b46 MKG |
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); | |
5b0d0914 MKG |
501 | if ( $dupcheck->id && ( !$self->id || $self->id != $dupcheck->id ) ) { |
502 | return ( 0, $self->loc( "Group name '[_1]' is already in use", $value ) ); | |
503 | } | |
84fb5b46 MKG |
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', | |
af59614d | 522 | Name => 'UserEquiv', |
84fb5b46 MKG |
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 | ||
af59614d | 550 | =head2 CreateRoleGroup |
84fb5b46 | 551 | |
af59614d | 552 | A convenience method for creating a role group on an object. |
84fb5b46 | 553 | |
af59614d MKG |
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. | |
84fb5b46 | 557 | |
af59614d MKG |
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. | |
84fb5b46 MKG |
599 | |
600 | =cut | |
601 | ||
602 | sub CreateRoleGroup { | |
603 | my $self = shift; | |
604 | my %args = ( Instance => undef, | |
af59614d | 605 | Name => undef, |
84fb5b46 | 606 | Domain => undef, |
af59614d MKG |
607 | Object => undef, |
608 | InsideTransaction => 1, | |
84fb5b46 MKG |
609 | @_ ); |
610 | ||
af59614d MKG |
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") ); | |
84fb5b46 MKG |
620 | } |
621 | ||
af59614d MKG |
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 | } | |
84fb5b46 | 718 | |
af59614d MKG |
719 | sub SetType { |
720 | my $self = shift; | |
721 | RT->Deprecated( Instead => 'Name', Remove => '4.4' ); | |
722 | return $self->SetName(@_); | |
84fb5b46 MKG |
723 | } |
724 | ||
af59614d MKG |
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; | |
84fb5b46 | 731 | |
af59614d MKG |
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 | } | |
84fb5b46 MKG |
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 && | |
af59614d MKG |
1008 | $self->CurrentUserHasRight('ModifyOwnMembership') ) || |
1009 | $self->CurrentUserHasRight('AdminGroupMembership') ) { | |
84fb5b46 MKG |
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 | |
af59614d | 1021 | # will get better |
84fb5b46 MKG |
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, | |
af59614d | 1029 | RecordTransaction => 1, |
84fb5b46 | 1030 | @_); |
af59614d MKG |
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 | ||
84fb5b46 MKG |
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 | ||
af59614d MKG |
1069 | my @purge; |
1070 | push @purge, @{$self->MembersObj->ItemsArrayRef} | |
1071 | if $self->SingleMemberRoleGroup; | |
84fb5b46 MKG |
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 | ); | |
af59614d MKG |
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; | |
84fb5b46 | 1095 | } |
af59614d MKG |
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 | } | |
84fb5b46 | 1128 | } |
af59614d MKG |
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) ); | |
84fb5b46 MKG |
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 | ||
af59614d MKG |
1230 | Optionally takes a hash of key value flags, such as RecordTransaction. |
1231 | ||
84fb5b46 MKG |
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) && | |
af59614d MKG |
1244 | $self->CurrentUserHasRight('ModifyOwnMembership') ) || |
1245 | $self->CurrentUserHasRight('AdminGroupMembership') ) { | |
84fb5b46 MKG |
1246 | #User has no permission to be doing this |
1247 | return ( 0, $self->loc("Permission Denied") ); | |
1248 | } | |
af59614d | 1249 | $self->_DeleteMember($member_id, @_); |
84fb5b46 MKG |
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 | |
af59614d | 1256 | # will get better |
84fb5b46 MKG |
1257 | |
1258 | sub _DeleteMember { | |
1259 | my $self = shift; | |
1260 | my $member_id = shift; | |
af59614d MKG |
1261 | my %args = ( |
1262 | RecordTransaction => 1, | |
1263 | @_, | |
1264 | ); | |
1265 | ||
84fb5b46 MKG |
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 | ||
af59614d MKG |
1279 | my $old_member = $member_obj->MemberId; |
1280 | ||
84fb5b46 MKG |
1281 | #Now that we've checked ACLs and sanity, delete the groupmember |
1282 | my $val = $member_obj->Delete(); | |
1283 | ||
af59614d | 1284 | unless ($val) { |
84fb5b46 MKG |
1285 | $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id); |
1286 | return ( 0, $self->loc("Member not deleted" )); | |
1287 | } | |
af59614d MKG |
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") ); | |
84fb5b46 MKG |
1316 | } |
1317 | ||
1318 | ||
1319 | ||
1320 | sub _Set { | |
1321 | my $self = shift; | |
1322 | my %args = ( | |
1323 | Field => undef, | |
1324 | Value => undef, | |
af59614d MKG |
1325 | TransactionType => 'Set', |
1326 | RecordTransaction => 1, | |
84fb5b46 MKG |
1327 | @_ |
1328 | ); | |
1329 | ||
1330 | unless ( $self->CurrentUserHasRight('AdminGroup') ) { | |
af59614d MKG |
1331 | return ( 0, $self->loc('Permission Denied') ); |
1332 | } | |
84fb5b46 MKG |
1333 | |
1334 | my $Old = $self->SUPER::_Value("$args{'Field'}"); | |
af59614d | 1335 | |
84fb5b46 | 1336 | my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'}, |
af59614d MKG |
1337 | Value => $args{'Value'} ); |
1338 | ||
84fb5b46 MKG |
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 | ||
84fb5b46 MKG |
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; | |
af59614d MKG |
1385 | my $res = RT::Principal->new( $self->CurrentUser ); |
1386 | $res->Load( $self->id ); | |
1387 | return $res; | |
84fb5b46 MKG |
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 | ||
af59614d MKG |
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 | } | |
84fb5b46 MKG |
1420 | |
1421 | sub BasicColumns { | |
1422 | ( | |
af59614d MKG |
1423 | [ Name => 'Name' ], |
1424 | [ Description => 'Description' ], | |
84fb5b46 MKG |
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 | ||
af59614d | 1511 | Deprecated, use Name instead, will be removed in 4.4. |
84fb5b46 MKG |
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 | ||
af59614d | 1520 | Deprecated, use SetName instead, will be removed in 4.4. |
84fb5b46 MKG |
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 => | |
af59614d | 1584 | {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, |
84fb5b46 | 1585 | Name => |
af59614d | 1586 | {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''}, |
84fb5b46 | 1587 | Description => |
af59614d | 1588 | {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, |
84fb5b46 | 1589 | Domain => |
af59614d | 1590 | {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, |
84fb5b46 | 1591 | Type => |
af59614d | 1592 | {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, |
84fb5b46 | 1593 | Instance => |
af59614d | 1594 | {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, |
84fb5b46 | 1595 | Creator => |
af59614d | 1596 | {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, |
84fb5b46 | 1597 | Created => |
af59614d | 1598 | {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, |
84fb5b46 | 1599 | LastUpdatedBy => |
af59614d | 1600 | {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, |
84fb5b46 | 1601 | LastUpdated => |
af59614d | 1602 | {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, |
84fb5b46 MKG |
1603 | |
1604 | } | |
1605 | }; | |
1606 | ||
af59614d MKG |
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}, Type => $data->{Type} ); | |
1684 | return $duplicated->() if $obj->Id; | |
1685 | } elsif ($data->{Domain} eq "RT::Queue-Role") { | |
1686 | $obj->LoadQueueRoleGroup( Queue => $data->{Instance}, Type => $data->{Type} ); | |
1687 | return $duplicated->() if $obj->Id; | |
1688 | } | |
1689 | ||
1690 | my $principal = RT::Principal->new( RT->SystemUser ); | |
1691 | my ($id) = $principal->Create( | |
1692 | PrincipalType => 'Group', | |
1693 | Disabled => $disabled, | |
1694 | ObjectId => 0, | |
1695 | ); | |
1696 | $importer->Resolve( $principal_uid => ref($principal), $id ); | |
1697 | ||
1698 | $importer->Postpone( | |
1699 | for => $uid, | |
1700 | uid => $principal_uid, | |
1701 | column => "ObjectId", | |
1702 | ); | |
1703 | ||
1704 | return 1; | |
1705 | } | |
1706 | ||
1707 | sub PostInflate { | |
1708 | my $self = shift; | |
1709 | ||
1710 | my $cgm = RT::CachedGroupMember->new($self->CurrentUser); | |
1711 | $cgm->Create( | |
1712 | Group => $self->PrincipalObj, | |
1713 | Member => $self->PrincipalObj, | |
1714 | ImmediateParent => $self->PrincipalObj | |
1715 | ); | |
1716 | } | |
1717 | ||
84fb5b46 MKG |
1718 | RT::Base->_ImportOverlays(); |
1719 | ||
1720 | 1; |