1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
52 package RT::Record::Role::Roles;
54 use Scalar::Util qw(blessed);
58 RT::Record::Role::Roles - Common methods for records which "watchers" or "roles"
62 =head2 L<RT::Record::Role>
66 with 'RT::Record::Role';
69 require RT::Principal;
73 require RT::EmailParser;
79 Registers an RT role which applies to this class for role-based access control.
86 Required. The role name (i.e. Requestor, Owner, AdminCc, etc).
90 Optional. Array ref of classes through which this role percolates up to
91 L<RT::System>. You can think of this list as:
93 map { ref } $record_object->ACLEquivalenceObjects;
95 You should not include L<RT::System> itself in this list.
97 Simply calls RegisterRole on each equivalent class.
101 Optional. A true value indicates that this role may only contain a single user
102 as a member at any given time. When adding a new member to a Single role, any
103 existing member will be removed. If all members are removed, L<RT/Nobody> is
108 Optional, implies Single. Specifies a column on the announcing class into
109 which the single role member's user ID is denormalized. The column will be
110 kept updated automatically as the role member changes. This is used, for
111 example, for ticket owners and makes searching simpler (among other benefits).
115 Optional. A true value indicates this role is only used for ACLs and should
116 not be populated with members.
118 This flag is advisory only, and the Perl API still allows members to be added
123 Optional. Automatically sets the ACLOnly flag for all EquivClasses, but not
124 the announcing class.
128 Optional. A numeric value indicating the position of this role when sorted
129 ascending with other roles in a list. Roles with the same sort order are
130 ordered alphabetically by name within themselves.
138 my $class = ref($self) || $self;
145 return unless $role{Name};
147 # Keep track of the class this role came from originally
148 $role{ Class } ||= $class;
150 # Some groups are limited to a single user
151 $role{ Single } = 1 if $role{Column};
153 # Stash the role on ourself
154 $class->_ROLES->{ $role{Name} } = { %role };
156 # Register it with any equivalent classes...
157 my $equiv = delete $role{EquivClasses} || [];
159 # ... and globally unless we ARE global
160 unless ($class eq "RT::System") {
161 push @$equiv, "RT::System";
164 # ... marked as "for ACLs only" if flagged as such by the announcing class
165 $role{ACLOnly} = 1 if delete $role{ACLOnlyInEquiv};
167 $_->RegisterRole(%role) for @$equiv;
169 # XXX TODO: Register which classes have roles on them somewhere?
174 =head2 UnregisterRole
176 Removes an RT role which applies to this class for role-based access control.
177 Any roles on equivalent classes (via EquivClasses passed to L</RegisterRole>)
178 are also unregistered.
180 Takes a role name as the sole argument.
182 B<Use this carefully:> Objects created after a role is unregistered will not
183 have an associated L<RT::Group> for the removed role. If you later decide to
184 stop unregistering the role, operations on those objects created in the
185 meantime will fail when trying to interact with the missing role groups.
187 B<Unregistering a role may break code which assumes the role exists.>
193 my $class = ref($self) || $self;
197 my $role = delete $self->_ROLES->{$name}
200 $_->UnregisterRole($name)
201 for "RT::System", reverse @{$role->{EquivClasses}};
206 Takes a role name; returns a hashref describing the role. This hashref
207 contains the same attributes used to register the role (see L</RegisterRole>),
208 as well as some extras, including:
214 The original class which announced the role. This is set automatically by
215 L</RegisterRole> and is the same across all EquivClasses.
219 Returns an empty hashref if the role doesn't exist.
224 return \%{ $_[0]->_ROLES->{$_[1]} || {} };
229 Returns a list of role names registered for this class, sorted ascending by
230 SortOrder and then alphabetically by name.
232 Optionally takes a hash specifying attributes the returned roles must possess
233 or lack. Testing is done on a simple truthy basis and the actual values of
234 the role attributes and arguments you pass are not compared string-wise or
235 numerically; they must simply evaluate to the same truthiness.
239 # Return role names which are not only for ACL purposes
240 $object->Roles( ACLOnly => 0 );
242 # Return role names which are denormalized into a column; note that the
243 # role's Column attribute contains a string.
244 $object->Roles( Column => 1 );
252 return map { $_->[0] }
253 sort { $a->[1]{SortOrder} <=> $b->[1]{SortOrder}
254 or $a->[0] cmp $b->[0] }
257 for my $k (keys %attr) {
258 $ok = 0, last if $attr{$k} xor $_->[1]{$k};
261 map { [ $_, $self->Role($_) ] }
262 keys %{ $self->_ROLES };
268 my $class = ref($_[0]) || $_[0];
269 return $ROLES{$class} ||= {};
275 Returns true if the name provided is a registered role for this class.
276 Otherwise returns false.
283 return scalar grep { $type eq $_ } $self->Roles;
288 Expects a role name as the first parameter which is used to load the
289 L<RT::Group> for the specified role on this record. Returns an unloaded
290 L<RT::Group> object on failure.
297 my $group = RT::Group->new( $self->CurrentUser );
299 if ($self->HasRole($name)) {
300 $group->LoadRoleGroup(
310 Adds the described L<RT::Principal> to the specified role group for this record.
312 Takes a set of key-value pairs:
318 Optional. The ID of the L<RT::Principal> object to add.
322 Optional. The Name or EmailAddress of an L<RT::User> to use as the
323 principal. If an email address is given, but a user matching it cannot
324 be found, a new user will be created.
328 Optional. The Name of an L<RT::Group> to use as the principal.
332 Required. One of the valid roles for this record, as returned by L</Roles>.
336 Optional. A subroutine reference which will be passed the role type and
337 principal being added. If it returns false, the method will fail with a
338 status of "Permission denied".
342 One, and only one, of I<PrincipalId>, I<User>, or I<Group> is required.
344 Returns a tuple of (principal object which was added, message).
352 return (0, $self->loc("One, and only one, of PrincipalId/User/Group is required"))
353 if 1 != grep { $_ } @args{qw/PrincipalId User Group/};
355 my $type = delete $args{Type};
356 return (0, $self->loc("No valid Type specified"))
357 unless $type and $self->HasRole($type);
359 if ($args{PrincipalId}) {
360 # Check the PrincipalId for loops
361 my $principal = RT::Principal->new( $self->CurrentUser );
362 $principal->Load($args{'PrincipalId'});
363 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
364 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop",
365 $email, $self->loc($type)))
366 if RT::EmailParser->IsRTAddress( $email );
370 my $name = delete $args{User};
371 # Sanity check the address
372 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop",
373 $name, $self->loc($type) ))
374 if RT::EmailParser->IsRTAddress( $name );
376 # Create as the SystemUser, not the current user
377 my $user = RT::User->new(RT->SystemUser);
380 ($ok, $msg) = $user->LoadOrCreateByEmail( $name );
382 ($ok, $msg) = $user->Load( $name );
385 # If we can't find this watcher, we need to bail.
386 $RT::Logger->error("Could not load or create a user '$name' to add as a watcher: $msg");
387 return (0, $self->loc("Could not find or create user '[_1]'", $name));
389 $args{PrincipalId} = $user->PrincipalId;
391 elsif ($args{Group}) {
392 my $name = delete $args{Group};
393 my $group = RT::Group->new( $self->CurrentUser );
394 $group->LoadUserDefinedGroup($name);
395 unless ($group->id) {
396 $RT::Logger->error("Could not load group '$name' to add as a watcher");
397 return (0, $self->loc("Could not find group '[_1]'", $name));
399 $args{PrincipalId} = $group->PrincipalObj->id;
403 my $principal = RT::Principal->new( $self->CurrentUser );
404 $principal->Load( $args{PrincipalId} );
406 my $acl = delete $args{ACL};
407 return (0, $self->loc("Permission denied"))
408 if $acl and not $acl->($type => $principal);
410 my $group = $self->RoleGroup( $type );
411 return (0, $self->loc("Role group '[_1]' not found", $type))
414 return (0, $self->loc('[_1] is already a [_2]',
415 $principal->Object->Name, $self->loc($type)) )
416 if $group->HasMember( $principal );
418 return (0, $self->loc('[_1] cannot be a group', $self->loc($type)) )
419 if $group->SingleMemberRoleGroup and $principal->IsGroup;
421 my ( $ok, $msg ) = $group->_AddMember( %args, RecordTransaction => !$args{Silent} );
423 $RT::Logger->error("Failed to add $args{PrincipalId} as a member of group ".$group->Id.": ".$msg);
425 return ( 0, $self->loc('Could not make [_1] a [_2]',
426 $principal->Object->Name, $self->loc($type)) );
429 return ($principal, $msg);
432 =head2 DeleteRoleMember
434 Removes the specified L<RT::Principal> from the specified role group for this
437 Takes a set of key-value pairs:
443 Optional. The ID of the L<RT::Principal> object to remove.
447 Optional. The Name or EmailAddress of an L<RT::User> to use as the
452 Required. One of the valid roles for this record, as returned by L</Roles>.
456 Optional. A subroutine reference which will be passed the role type and
457 principal being removed. If it returns false, the method will fail with a
458 status of "Permission denied".
462 One, and only one, of I<PrincipalId> or I<User> is required.
464 Returns a tuple of (principal object that was removed, message).
468 sub DeleteRoleMember {
472 return (0, $self->loc("No valid Type specified"))
473 unless $args{Type} and $self->HasRole($args{Type});
476 my $user = RT::User->new( $self->CurrentUser );
477 $user->LoadByEmail( $args{User} );
478 $user->Load( $args{User} ) unless $user->id;
479 return (0, $self->loc("Could not load user '[_1]'", $args{User}) )
481 $args{PrincipalId} = $user->PrincipalId;
484 return (0, $self->loc("No valid PrincipalId"))
485 unless $args{PrincipalId};
487 my $principal = RT::Principal->new( $self->CurrentUser );
488 $principal->Load( $args{PrincipalId} );
490 my $acl = delete $args{ACL};
491 return (0, $self->loc("Permission denied"))
492 if $acl and not $acl->($args{Type} => $principal);
494 my $group = $self->RoleGroup( $args{Type} );
495 return (0, $self->loc("Role group '[_1]' not found", $args{Type}))
498 return ( 0, $self->loc( '[_1] is not a [_2]',
499 $principal->Object->Name, $self->loc($args{Type}) ) )
500 unless $group->HasMember($principal);
502 my ($ok, $msg) = $group->_DeleteMember($args{PrincipalId}, RecordTransaction => !$args{Silent});
504 $RT::Logger->error("Failed to remove $args{PrincipalId} as a member of group ".$group->Id.": ".$msg);
506 return ( 0, $self->loc('Could not remove [_1] as a [_2]',
507 $principal->Object->Name, $self->loc($args{Type})) );
510 return ($principal, $msg);
515 my ($roles, %args) = (@_);
518 for my $role ($self->Roles) {
519 if ($self->_ROLES->{$role}{Single}) {
520 # Default to nobody if unspecified
521 my $value = $args{$role} || RT->Nobody;
522 $value = $value->[0] if ref $value eq 'ARRAY';
523 if (Scalar::Util::blessed($value) and $value->isa("RT::User")) {
524 # Accept a user; it may not be loaded, which we catch below
525 $roles->{$role} = $value->PrincipalObj;
527 # Try loading by id, name, then email. If all fail, catch that below
528 my $user = RT::User->new( $self->CurrentUser );
529 $user->Load( $value );
530 # XXX: LoadOrCreateByEmail ?
531 $user->LoadByEmail( $value ) unless $user->id;
532 $roles->{$role} = $user->PrincipalObj;
534 unless (Scalar::Util::blessed($roles->{$role}) and $roles->{$role}->id) {
535 push @errors, $self->loc("Invalid value for [_1]",$self->loc($role));
536 $roles->{$role} = RT->Nobody->PrincipalObj;
538 # For consistency, we always return an arrayref
539 $roles->{$role} = [ $roles->{$role} ];
541 $roles->{$role} = [];
542 my @values = ref $args{ $role } ? @{ $args{$role} } : ($args{$role});
543 for my $value (grep {defined} @values) {
544 if ( $value =~ /^\d+$/ ) {
545 # This implicitly allows groups, if passed by id.
546 my $principal = RT::Principal->new( $self->CurrentUser );
547 my ($ok, $msg) = $principal->Load( $value );
549 push @{ $roles->{$role} }, $principal;
552 $self->loc("Couldn't load principal: [_1]", $msg);
555 my @addresses = RT::EmailParser->ParseEmailAddress( $value );
556 for my $address ( @addresses ) {
557 my $user = RT::User->new( RT->SystemUser );
558 my ($id, $msg) = $user->LoadOrCreateByEmail( $address );
560 # Load it back as us, not as the system
561 # user, to be completely safe.
562 $user = RT::User->new( $self->CurrentUser );
564 push @{ $roles->{$role} }, $user->PrincipalObj;
567 $self->loc("Couldn't load or create user: [_1]", $msg);
577 sub _CreateRoleGroups {
580 for my $name ($self->Roles) {
581 my $type_obj = RT::Group->new($self->CurrentUser);
582 my ($id, $msg) = $type_obj->CreateRoleGroup(
588 $RT::Logger->error("Couldn't create a role group of type '$name' for ".ref($self)." ".
589 $self->id.": ".$msg);
596 sub _AddRolesOnCreate {
598 my ($roles, %acls) = @_;
604 for my $role (keys %{$roles}) {
605 my $group = $self->RoleGroup($role);
607 for my $principal (@{$roles->{$role}}) {
608 if ($acls{$role}->($principal)) {
609 next if $group->HasMember($principal);
610 my ($ok, $msg) = $group->_AddMember(
611 PrincipalId => $principal->id,
612 InsideTransaction => 1,
613 RecordTransaction => 0,
616 push @errors, $self->loc("Couldn't set [_1] watcher: [_2]", $role, $msg)
620 push @left, $principal;
623 $roles->{$role} = [ @left ];