diff options
Diffstat (limited to 'rt/lib/RT/Record/Role')
-rw-r--r-- | rt/lib/RT/Record/Role/Lifecycle.pm | 219 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Links.pm | 174 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Rights.pm | 133 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Roles.pm | 633 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Status.pm | 314 |
5 files changed, 1473 insertions, 0 deletions
diff --git a/rt/lib/RT/Record/Role/Lifecycle.pm b/rt/lib/RT/Record/Role/Lifecycle.pm new file mode 100644 index 0000000..0474a06 --- /dev/null +++ b/rt/lib/RT/Record/Role/Lifecycle.pm @@ -0,0 +1,219 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Record::Role::Lifecycle; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Lifecycle - Common methods for records which have a Lifecycle column + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=head2 LifecycleType + +Used as a role parameter. Must return a string of the type of lifecycles the +record consumes, i.e. I<ticket> for L<RT::Queue>. + +=head2 Lifecycle + +A Lifecycle method which returns a lifecycle name is required. Currently +unenforced at compile-time due to poor interactions with +L<DBIx::SearchBuilder::Record/AUTOLOAD>. You'll hit run-time errors if this +method isn't available in consuming classes, however. + +=cut + +with 'RT::Record::Role'; +requires 'LifecycleType'; + +# XXX: can't require column methods due to DBIx::SB::Record's AUTOLOAD +#requires 'Lifecycle'; + +=head1 PROVIDES + +=head2 LifecycleObj + +Returns an L<RT::Lifecycle> object for this record's C<Lifecycle>. If called +as a class method, returns an L<RT::Lifecycle> object which is an aggregation +of all lifecycles of the appropriate type. + +=cut + +sub LifecycleObj { + my $self = shift; + my $type = $self->LifecycleType; + my $fallback = $self->_Accessible( Lifecycle => "default" ); + + unless (blessed($self) and $self->id) { + return RT::Lifecycle->Load( Type => $type ); + } + + my $name = $self->Lifecycle || $fallback; + my $res = RT::Lifecycle->Load( Name => $name, Type => $type ); + unless ( $res ) { + RT->Logger->error( + sprintf "Lifecycle '%s' of type %s for %s #%d doesn't exist", + $name, $type, ref($self), $self->id); + return RT::Lifecycle->Load( Name => $fallback, Type => $type ); + } + return $res; +} + +=head2 SetLifecycle + +Validates that the specified lifecycle exists before updating the record. + +Takes a lifecycle name. + +=cut + +sub SetLifecycle { + my $self = shift; + my $value = shift || $self->_Accessible( Lifecycle => "default" ); + + return (0, $self->loc('[_1] is not a valid lifecycle', $value)) + unless $self->ValidateLifecycle($value); + + return $self->_Set( Field => 'Lifecycle', Value => $value, @_ ); +} + +=head2 ValidateLifecycle + +Takes a lifecycle name. Returns true if it's an OK name and such lifecycle is +configured. Returns false otherwise. + +=cut + +sub ValidateLifecycle { + my $self = shift; + my $value = shift; + return unless $value; + return unless RT::Lifecycle->Load( Name => $value, Type => $self->LifecycleType ); + return 1; +} + +=head2 ActiveStatusArray + +Returns an array of all ActiveStatuses for the lifecycle + +=cut + +sub ActiveStatusArray { + my $self = shift; + return $self->LifecycleObj->Valid('initial', 'active'); +} + +=head2 InactiveStatusArray + +Returns an array of all InactiveStatuses for the lifecycle + +=cut + +sub InactiveStatusArray { + my $self = shift; + return $self->LifecycleObj->Inactive; +} + +=head2 StatusArray + +Returns an array of all statuses for the lifecycle + +=cut + +sub StatusArray { + my $self = shift; + return $self->LifecycleObj->Valid( @_ ); +} + +=head2 IsValidStatus + +Takes a status. + +Returns true if STATUS is a valid status. Otherwise, returns 0. + +=cut + +sub IsValidStatus { + my $self = shift; + return $self->LifecycleObj->IsValid( shift ); +} + +=head2 IsActiveStatus + +Takes a status. + +Returns true if STATUS is a Active status. Otherwise, returns 0 + +=cut + +sub IsActiveStatus { + my $self = shift; + return $self->LifecycleObj->IsValid( shift, 'initial', 'active'); +} + +=head2 IsInactiveStatus + +Takes a status. + +Returns true if STATUS is a Inactive status. Otherwise, returns 0 + +=cut + +sub IsInactiveStatus { + my $self = shift; + return $self->LifecycleObj->IsInactive( shift ); +} + +1; diff --git a/rt/lib/RT/Record/Role/Links.pm b/rt/lib/RT/Record/Role/Links.pm new file mode 100644 index 0000000..f865090 --- /dev/null +++ b/rt/lib/RT/Record/Role/Links.pm @@ -0,0 +1,174 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Record::Role::Links; +use Role::Basic; + +=head1 NAME + +RT::Record::Role::Links - Common methods for records which handle links + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=head2 _AddLink + +Usually provided by L<RT::Record/_AddLink>. + +=head2 _DeleteLink + +Usually provided by L<RT::Record/_DeleteLink>. + +=head2 ModifyLinkRight + +The right name to check in L<AddLink> and L<DeleteLink>. + +=head2 CurrentUserHasRight + +=cut + +with 'RT::Record::Role'; + +requires '_AddLink'; +requires '_DeleteLink'; + +requires 'ModifyLinkRight'; +requires 'CurrentUserHasRight'; + +=head1 PROVIDES + +=head2 _AddLinksOnCreate + +Calls _AddLink (usually L<RT::Record/_AddLink>) for all valid link types and +aliases found in the hash. Refer to L<RT::Link/%TYPEMAP> for details of link +types. Key values may be a single URI or an arrayref of URIs. + +Takes two hashrefs. The first is the argument hash provided to the consuming +class's Create method. The second is optional and contains extra arguments to +pass to _AddLink. + +By default records a transaction on the link's destination object (if any), but +not on the origin object. + +Returns an array of localized error messages, if any. + +=cut + +sub _AddLinksOnCreate { + my $self = shift; + my %args = %{shift || {}}; + my %AddLink = %{shift || {}}; + my @results; + + foreach my $type ( keys %RT::Link::TYPEMAP ) { + next unless defined $args{$type}; + + my $links = $args{$type}; + $links = [$links] unless ref $links; + + for my $link (@$links) { + my $typemap = $RT::Link::TYPEMAP{$type}; + my $opposite_mode = $typemap->{Mode} eq "Base" ? "Target" : "Base"; + my ($ok, $msg) = $self->_AddLink( + Type => $typemap->{Type}, + $typemap->{Mode} => $link, + "Silent$opposite_mode" => 1, + %AddLink, + ); + push @results, + $self->loc("Unable to add [_1] link: [_2]", $self->loc($type), $msg) + unless $ok; + } + } + return @results; +} + +=head2 AddLink + +Takes a paramhash of Type and one of Base or Target. Adds that link to this +record. + +Refer to L<RT::Record/_AddLink> for full documentation. This method implements +permissions and ticket validity checks before calling into L<RT::Record> +(usually). + +=cut + +sub AddLink { + my $self = shift; + + return (0, $self->loc("Permission Denied")) + unless $self->CurrentUserHasRight($self->ModifyLinkRight); + + return $self->_AddLink(@_); +} + +=head2 DeleteLink + +Takes a paramhash of Type and one of Base or Target. Removes that link from the +record. + +Refer to L<RT::Record/_DeleteLink> for full documentation. This method +implements permission checks before calling into L<RT::Record> (usually). + +=cut + +sub DeleteLink { + my $self = shift; + + return (0, $self->loc("Permission Denied")) + unless $self->CurrentUserHasRight($self->ModifyLinkRight); + + return $self->_DeleteLink(@_); +} + +1; diff --git a/rt/lib/RT/Record/Role/Rights.pm b/rt/lib/RT/Record/Role/Rights.pm new file mode 100644 index 0000000..cd2d60a --- /dev/null +++ b/rt/lib/RT/Record/Role/Rights.pm @@ -0,0 +1,133 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Record::Role::Rights; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Rights - Common methods for records which can provide rights + +=head1 DESCRIPTION + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=cut + +with 'RT::Record::Role'; + +=head1 PROVIDES + +=cut + +=head2 AddRight C<CATEGORY>, C<RIGHT>, C<DESCRIPTION> + +Adds the given rights to the list of possible rights. This method +should be called during server startup, not at runtime. + +=cut + +sub AddRight { + my $class = shift; + $class = ref($class) || $class; + my ($category, $name, $description) = @_; + + require RT::ACE; + if (exists $RT::ACE::RIGHTS{$class}{lc $name}) { + warn "Duplicate right '$name' found"; + return; + } + + $RT::ACE::RIGHTS{$class}{lc $name} = { + Name => $name, + Category => $category, + Description => $description, + }; +} + +=head2 AvailableRights + +Returns a hashref of available rights for this object. The keys are the +right names and the values are a description of what the rights do. + +=cut + +sub AvailableRights { + my $self = shift; + my $class = ref($self) || $self; + + my %rights; + $rights{$_->{Name}} = $_->{Description} + for values %{$RT::ACE::RIGHTS{$class} || {} }; + return \%rights; +} + +=head2 RightCategories + +Returns a hashref where the keys are rights for this type of object and the +values are the category (General, Staff, Admin) the right falls into. + +=cut + +sub RightCategories { + my $self = shift; + my $class = ref($self) || $self; + + my %rights; + $rights{$_->{Name}} = $_->{Category} + for values %{ $RT::ACE::RIGHTS{$class} || {} }; + return \%rights; +} + +1; diff --git a/rt/lib/RT/Record/Role/Roles.pm b/rt/lib/RT/Record/Role/Roles.pm new file mode 100644 index 0000000..725c0d7 --- /dev/null +++ b/rt/lib/RT/Record/Role/Roles.pm @@ -0,0 +1,633 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Record::Role::Roles; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Roles - Common methods for records which "watchers" or "roles" + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=cut + +with 'RT::Record::Role'; + +require RT::System; +require RT::Principal; +require RT::Group; +require RT::User; + +require RT::EmailParser; + +=head1 PROVIDES + +=head2 RegisterRole + +Registers an RT role which applies to this class for role-based access control. +Arguments: + +=over 4 + +=item Name + +Required. The role name (i.e. Requestor, Owner, AdminCc, etc). + +=item EquivClasses + +Optional. Array ref of classes through which this role percolates up to +L<RT::System>. You can think of this list as: + + map { ref } $record_object->ACLEquivalenceObjects; + +You should not include L<RT::System> itself in this list. + +Simply calls RegisterRole on each equivalent class. + +=item Single + +Optional. A true value indicates that this role may only contain a single user +as a member at any given time. When adding a new member to a Single role, any +existing member will be removed. If all members are removed, L<RT/Nobody> is +added automatically. + +=item Column + +Optional, implies Single. Specifies a column on the announcing class into +which the single role member's user ID is denormalized. The column will be +kept updated automatically as the role member changes. This is used, for +example, for ticket owners and makes searching simpler (among other benefits). + +=item ACLOnly + +Optional. A true value indicates this role is only used for ACLs and should +not be populated with members. + +This flag is advisory only, and the Perl API still allows members to be added +to ACLOnly roles. + +=item ACLOnlyInEquiv + +Optional. Automatically sets the ACLOnly flag for all EquivClasses, but not +the announcing class. + +=item SortOrder + +Optional. A numeric value indicating the position of this role when sorted +ascending with other roles in a list. Roles with the same sort order are +ordered alphabetically by name within themselves. + +=back + +=cut + +sub RegisterRole { + my $self = shift; + my $class = ref($self) || $self; + my %role = ( + Name => undef, + EquivClasses => [], + SortOrder => 0, + @_ + ); + return unless $role{Name}; + + # Keep track of the class this role came from originally + $role{ Class } ||= $class; + + # Some groups are limited to a single user + $role{ Single } = 1 if $role{Column}; + + # Stash the role on ourself + $class->_ROLES->{ $role{Name} } = { %role }; + + # Register it with any equivalent classes... + my $equiv = delete $role{EquivClasses} || []; + + # ... and globally unless we ARE global + unless ($class eq "RT::System") { + push @$equiv, "RT::System"; + } + + # ... marked as "for ACLs only" if flagged as such by the announcing class + $role{ACLOnly} = 1 if delete $role{ACLOnlyInEquiv}; + + $_->RegisterRole(%role) for @$equiv; + + # XXX TODO: Register which classes have roles on them somewhere? + + return 1; +} + +=head2 UnregisterRole + +Removes an RT role which applies to this class for role-based access control. +Any roles on equivalent classes (via EquivClasses passed to L</RegisterRole>) +are also unregistered. + +Takes a role name as the sole argument. + +B<Use this carefully:> Objects created after a role is unregistered will not +have an associated L<RT::Group> for the removed role. If you later decide to +stop unregistering the role, operations on those objects created in the +meantime will fail when trying to interact with the missing role groups. + +B<Unregistering a role may break code which assumes the role exists.> + +=cut + +sub UnregisterRole { + my $self = shift; + my $class = ref($self) || $self; + my $name = shift + or return; + + my $role = delete $self->_ROLES->{$name} + or return; + + $_->UnregisterRole($name) + for "RT::System", reverse @{$role->{EquivClasses}}; +} + +=head2 Role + +Takes a role name; returns a hashref describing the role. This hashref +contains the same attributes used to register the role (see L</RegisterRole>), +as well as some extras, including: + +=over + +=item Class + +The original class which announced the role. This is set automatically by +L</RegisterRole> and is the same across all EquivClasses. + +=back + +Returns an empty hashref if the role doesn't exist. + +=cut + +sub Role { + return \%{ $_[0]->_ROLES->{$_[1]} || {} }; +} + +=head2 Roles + +Returns a list of role names registered for this class, sorted ascending by +SortOrder and then alphabetically by name. + +Optionally takes a hash specifying attributes the returned roles must possess +or lack. Testing is done on a simple truthy basis and the actual values of +the role attributes and arguments you pass are not compared string-wise or +numerically; they must simply evaluate to the same truthiness. + +For example: + + # Return role names which are not only for ACL purposes + $object->Roles( ACLOnly => 0 ); + + # Return role names which are denormalized into a column; note that the + # role's Column attribute contains a string. + $object->Roles( Column => 1 ); + +=cut + +sub Roles { + my $self = shift; + my %attr = @_; + + return map { $_->[0] } + sort { $a->[1]{SortOrder} <=> $b->[1]{SortOrder} + or $a->[0] cmp $b->[0] } + grep { + my $ok = 1; + for my $k (keys %attr) { + $ok = 0, last if $attr{$k} xor $_->[1]{$k}; + } + $ok } + map { [ $_, $self->Role($_) ] } + keys %{ $self->_ROLES }; +} + +{ + my %ROLES; + sub _ROLES { + my $class = ref($_[0]) || $_[0]; + return $ROLES{$class} ||= {}; + } +} + +=head2 HasRole + +Returns true if the name provided is a registered role for this class. +Otherwise returns false. + +=cut + +sub HasRole { + my $self = shift; + my $type = shift; + return scalar grep { $type eq $_ } $self->Roles; +} + +=head2 RoleGroup + +Expects a role name as the first parameter which is used to load the +L<RT::Group> for the specified role on this record. Returns an unloaded +L<RT::Group> object on failure. + +=cut + +sub RoleGroup { + my $self = shift; + my $name = shift; + my $group = RT::Group->new( $self->CurrentUser ); + + if ($self->HasRole($name)) { + $group->LoadRoleGroup( + Object => $self, + Name => $name, + ); + } + return $group; +} + +=head2 AddRoleMember + +Adds the described L<RT::Principal> to the specified role group for this record. + +Takes a set of key-value pairs: + +=over 4 + +=item PrincipalId + +Optional. The ID of the L<RT::Principal> object to add. + +=item User + +Optional. The Name or EmailAddress of an L<RT::User> to use as the +principal. If an email address is given, but a user matching it cannot +be found, a new user will be created. + +=item Group + +Optional. The Name of an L<RT::Group> to use as the principal. + +=item Type + +Required. One of the valid roles for this record, as returned by L</Roles>. + +=item ACL + +Optional. A subroutine reference which will be passed the role type and +principal being added. If it returns false, the method will fail with a +status of "Permission denied". + +=back + +One, and only one, of I<PrincipalId>, I<User>, or I<Group> is required. + +Returns a tuple of (principal object which was added, message). + +=cut + +sub AddRoleMember { + my $self = shift; + my %args = (@_); + + return (0, $self->loc("One, and only one, of PrincipalId/User/Group is required")) + if 1 != grep { $_ } @args{qw/PrincipalId User Group/}; + + my $type = delete $args{Type}; + return (0, $self->loc("No valid Type specified")) + unless $type and $self->HasRole($type); + + if ($args{PrincipalId}) { + # Check the PrincipalId for loops + my $principal = RT::Principal->new( $self->CurrentUser ); + $principal->Load($args{'PrincipalId'}); + if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) { + return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", + $email, $self->loc($type))) + if RT::EmailParser->IsRTAddress( $email ); + } + } else { + if ($args{User}) { + my $name = delete $args{User}; + # Sanity check the address + return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", + $name, $self->loc($type) )) + if RT::EmailParser->IsRTAddress( $name ); + + # Create as the SystemUser, not the current user + my $user = RT::User->new(RT->SystemUser); + my ($ok, $msg); + if ($name =~ /@/) { + ($ok, $msg) = $user->LoadOrCreateByEmail( $name ); + } else { + ($ok, $msg) = $user->Load( $name ); + } + unless ($user->Id) { + # If we can't find this watcher, we need to bail. + $RT::Logger->error("Could not load or create a user '$name' to add as a watcher: $msg"); + return (0, $self->loc("Could not find or create user '[_1]'", $name)); + } + $args{PrincipalId} = $user->PrincipalId; + } + elsif ($args{Group}) { + my $name = delete $args{Group}; + my $group = RT::Group->new( $self->CurrentUser ); + $group->LoadUserDefinedGroup($name); + unless ($group->id) { + $RT::Logger->error("Could not load group '$name' to add as a watcher"); + return (0, $self->loc("Could not find group '[_1]'", $name)); + } + $args{PrincipalId} = $group->PrincipalObj->id; + } + } + + my $principal = RT::Principal->new( $self->CurrentUser ); + $principal->Load( $args{PrincipalId} ); + + my $acl = delete $args{ACL}; + return (0, $self->loc("Permission denied")) + if $acl and not $acl->($type => $principal); + + my $group = $self->RoleGroup( $type ); + return (0, $self->loc("Role group '[_1]' not found", $type)) + unless $group->id; + + return (0, $self->loc('[_1] is already a [_2]', + $principal->Object->Name, $self->loc($type)) ) + if $group->HasMember( $principal ); + + return (0, $self->loc('[_1] cannot be a group', $self->loc($type)) ) + if $group->SingleMemberRoleGroup and $principal->IsGroup; + + my ( $ok, $msg ) = $group->_AddMember( %args, RecordTransaction => !$args{Silent} ); + unless ($ok) { + $RT::Logger->error("Failed to add $args{PrincipalId} as a member of group ".$group->Id.": ".$msg); + + return ( 0, $self->loc('Could not make [_1] a [_2]', + $principal->Object->Name, $self->loc($type)) ); + } + + return ($principal, $msg); +} + +=head2 DeleteRoleMember + +Removes the specified L<RT::Principal> from the specified role group for this +record. + +Takes a set of key-value pairs: + +=over 4 + +=item PrincipalId + +Optional. The ID of the L<RT::Principal> object to remove. + +=item User + +Optional. The Name or EmailAddress of an L<RT::User> to use as the +principal + +=item Type + +Required. One of the valid roles for this record, as returned by L</Roles>. + +=item ACL + +Optional. A subroutine reference which will be passed the role type and +principal being removed. If it returns false, the method will fail with a +status of "Permission denied". + +=back + +One, and only one, of I<PrincipalId> or I<User> is required. + +Returns a tuple of (principal object that was removed, message). + +=cut + +sub DeleteRoleMember { + my $self = shift; + my %args = (@_); + + return (0, $self->loc("No valid Type specified")) + unless $args{Type} and $self->HasRole($args{Type}); + + if ($args{User}) { + my $user = RT::User->new( $self->CurrentUser ); + $user->LoadByEmail( $args{User} ); + $user->Load( $args{User} ) unless $user->id; + return (0, $self->loc("Could not load user '[_1]'", $args{User}) ) + unless $user->id; + $args{PrincipalId} = $user->PrincipalId; + } + + return (0, $self->loc("No valid PrincipalId")) + unless $args{PrincipalId}; + + my $principal = RT::Principal->new( $self->CurrentUser ); + $principal->Load( $args{PrincipalId} ); + + my $acl = delete $args{ACL}; + return (0, $self->loc("Permission denied")) + if $acl and not $acl->($args{Type} => $principal); + + my $group = $self->RoleGroup( $args{Type} ); + return (0, $self->loc("Role group '[_1]' not found", $args{Type})) + unless $group->id; + + return ( 0, $self->loc( '[_1] is not a [_2]', + $principal->Object->Name, $self->loc($args{Type}) ) ) + unless $group->HasMember($principal); + + my ($ok, $msg) = $group->_DeleteMember($args{PrincipalId}, RecordTransaction => !$args{Silent}); + unless ($ok) { + $RT::Logger->error("Failed to remove $args{PrincipalId} as a member of group ".$group->Id.": ".$msg); + + return ( 0, $self->loc('Could not remove [_1] as a [_2]', + $principal->Object->Name, $self->loc($args{Type})) ); + } + + return ($principal, $msg); +} + +sub _ResolveRoles { + my $self = shift; + my ($roles, %args) = (@_); + + my @errors; + for my $role ($self->Roles) { + if ($self->_ROLES->{$role}{Single}) { + # Default to nobody if unspecified + my $value = $args{$role} || RT->Nobody; + $value = $value->[0] if ref $value eq 'ARRAY'; + if (Scalar::Util::blessed($value) and $value->isa("RT::User")) { + # Accept a user; it may not be loaded, which we catch below + $roles->{$role} = $value->PrincipalObj; + } else { + # Try loading by id, name, then email. If all fail, catch that below + my $user = RT::User->new( $self->CurrentUser ); + $user->Load( $value ); + # XXX: LoadOrCreateByEmail ? + $user->LoadByEmail( $value ) unless $user->id; + $roles->{$role} = $user->PrincipalObj; + } + unless (Scalar::Util::blessed($roles->{$role}) and $roles->{$role}->id) { + push @errors, $self->loc("Invalid value for [_1]",$self->loc($role)); + $roles->{$role} = RT->Nobody->PrincipalObj; + } + # For consistency, we always return an arrayref + $roles->{$role} = [ $roles->{$role} ]; + } else { + $roles->{$role} = []; + my @values = ref $args{ $role } ? @{ $args{$role} } : ($args{$role}); + for my $value (grep {defined} @values) { + if ( $value =~ /^\d+$/ ) { + # This implicitly allows groups, if passed by id. + my $principal = RT::Principal->new( $self->CurrentUser ); + my ($ok, $msg) = $principal->Load( $value ); + if ($ok) { + push @{ $roles->{$role} }, $principal; + } else { + push @errors, + $self->loc("Couldn't load principal: [_1]", $msg); + } + } else { + my @addresses = RT::EmailParser->ParseEmailAddress( $value ); + for my $address ( @addresses ) { + my $user = RT::User->new( RT->SystemUser ); + my ($id, $msg) = $user->LoadOrCreateByEmail( $address ); + if ( $id ) { + # Load it back as us, not as the system + # user, to be completely safe. + $user = RT::User->new( $self->CurrentUser ); + $user->Load( $id ); + push @{ $roles->{$role} }, $user->PrincipalObj; + } else { + push @errors, + $self->loc("Couldn't load or create user: [_1]", $msg); + } + } + } + } + } + } + return (@errors); +} + +sub _CreateRoleGroups { + my $self = shift; + my %args = (@_); + for my $name ($self->Roles) { + my $type_obj = RT::Group->new($self->CurrentUser); + my ($id, $msg) = $type_obj->CreateRoleGroup( + Name => $name, + Object => $self, + %args, + ); + unless ($id) { + $RT::Logger->error("Couldn't create a role group of type '$name' for ".ref($self)." ". + $self->id.": ".$msg); + return(undef); + } + } + return(1); +} + +sub _AddRolesOnCreate { + my $self = shift; + my ($roles, %acls) = @_; + + my @errors; + { + my $changed = 0; + + for my $role (keys %{$roles}) { + my $group = $self->RoleGroup($role); + my @left; + for my $principal (@{$roles->{$role}}) { + if ($acls{$role}->($principal)) { + next if $group->HasMember($principal); + my ($ok, $msg) = $group->_AddMember( + PrincipalId => $principal->id, + InsideTransaction => 1, + RecordTransaction => 0, + Object => $self, + ); + push @errors, $self->loc("Couldn't set [_1] watcher: [_2]", $role, $msg) + unless $ok; + $changed++; + } else { + push @left, $principal; + } + } + $roles->{$role} = [ @left ]; + } + + redo if $changed; + } + + return @errors; +} + + +1; diff --git a/rt/lib/RT/Record/Role/Status.pm b/rt/lib/RT/Record/Role/Status.pm new file mode 100644 index 0000000..98f699c --- /dev/null +++ b/rt/lib/RT/Record/Role/Status.pm @@ -0,0 +1,314 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Record::Role::Status; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Status - Common methods for records which have a Status column + +=head1 DESCRIPTION + +Lifecycles are generally set on container records, and Statuses on records +which belong to one of those containers. L<RT::Record::Role::Lifecycle> +handles the containers with the I<Lifecycle> column. This role is for the +records with a I<Status> column within those containers. It includes +convenience methods for grabbing an L<RT::Lifecycle> object as well setters for +validating I<Status> and the column which points to the container object. + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=head2 LifecycleColumn + +Used as a role parameter. Must return a string of the column name which points +to the container object that consumes L<RT::Record::Role::Lifecycle> (or +conforms to it). The resulting string is used to construct two method names: +as-is to fetch the column value and suffixed with "Obj" to fetch the object. + +=head2 Status + +A Status method which returns a lifecycle name is required. Currently +unenforced at compile-time due to poor interactions with +L<DBIx::SearchBuilder::Record/AUTOLOAD>. You'll hit run-time errors if this +method isn't available in consuming classes, however. + +=cut + +with 'RT::Record::Role'; +requires 'LifecycleColumn'; + +=head1 PROVIDES + +=head2 Status + +Returns the Status for this record, in the canonical casing. + +=cut + +sub Status { + my $self = shift; + my $value = $self->_Value( 'Status' ); + my $lifecycle = $self->LifecycleObj; + return $value unless $lifecycle; + return $lifecycle->CanonicalCase( $value ); +} + +=head2 LifecycleObj + +Returns an L<RT::Lifecycle> object for this record's C<Lifecycle>. If called +as a class method, returns an L<RT::Lifecycle> object which is an aggregation +of all lifecycles of the appropriate type. + +=cut + +sub LifecycleObj { + my $self = shift; + my $obj = $self->LifecycleColumn . "Obj"; + return $self->$obj->LifecycleObj; +} + +=head2 Lifecycle + +Returns the L<RT::Lifecycle/Name> of this record's L</LifecycleObj>. + +=cut + +sub Lifecycle { + my $self = shift; + return $self->LifecycleObj->Name; +} + +=head2 ValidateStatus + +Takes a status. Returns true if that status is a valid status for this record, +otherwise returns false. + +=cut + +sub ValidateStatus { + my $self = shift; + return $self->LifecycleObj->IsValid(@_); +} + +=head2 ValidateStatusChange + +Validates the new status with the current lifecycle. Returns a tuple of (OK, +message). + +Expected to be called from this role's L</SetStatus> or the consuming class' +equivalent. + +=cut + +sub ValidateStatusChange { + my $self = shift; + my $new = shift; + my $old = $self->Status; + + my $lifecycle = $self->LifecycleObj; + + unless ( $lifecycle->IsValid( $new ) ) { + return (0, $self->loc("Status '[_1]' isn't a valid status for this [_2].", $self->loc($new), $self->loc($lifecycle->Type))); + } + + unless ( $lifecycle->IsTransition( $old => $new ) ) { + return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new))); + } + + my $check_right = $lifecycle->CheckRight( $old => $new ); + unless ( $self->CurrentUser->HasRight( Right => $check_right, Object => $self ) ) { + return ( 0, $self->loc('Permission Denied') ); + } + + return 1; +} + +=head2 SetStatus + +Validates the status transition before updating the Status column. This method +may want to be overridden by a more specific method in the consuming class. + +=cut + +sub SetStatus { + my $self = shift; + my $new = shift; + + my ($valid, $error) = $self->ValidateStatusChange($new); + return ($valid, $error) unless $valid; + + return $self->_SetStatus( Status => $new ); +} + +=head2 _SetStatus + +Sets the Status column without validating the change. Intended to be used +as-is by methods provided by the role, or overridden in the consuming class to +take additional action. For example, L<RT::Ticket/_SetStatus> sets the Started +and Resolved dates on the ticket as necessary. + +Takes a paramhash where the only required key is Status. Other keys may +include Lifecycle and NewLifecycle when called from L</_SetLifecycleColumn>, +which may assist consuming classes. NewLifecycle defaults to Lifecycle if not +provided; this indicates the lifecycle isn't changing. + +=cut + +sub _SetStatus { + my $self = shift; + my %args = ( + Status => undef, + Lifecycle => $self->LifecycleObj, + @_, + ); + $args{Status} = lc $args{Status} if defined $args{Status}; + $args{NewLifecycle} ||= $args{Lifecycle}; + + return $self->_Set( + Field => 'Status', + Value => $args{Status}, + ); +} + +=head2 _SetLifecycleColumn + +Validates and updates the column named by L</LifecycleColumn>. The Status +column is also updated if necessary (via lifecycle transition maps). + +On success, returns a tuple of (1, I<message>, I<new status>) where I<new +status> is the status that was transitioned to, if any. On failure, returns +(0, I<error message>). + +Takes a paramhash with keys I<Value> and (optionally) I<RequireRight>. +I<RequireRight> is a right name which the current user must have on the new +L</LifecycleColumn> object in order for the method to succeed. + +This method is expected to be used from within another method such as +L<RT::Ticket/SetQueue>. + +=cut + +sub _SetLifecycleColumn { + my $self = shift; + my %args = @_; + + my $column = $self->LifecycleColumn; + my $column_obj = "${column}Obj"; + + my $current = $self->$column_obj; + my $class = blessed($current); + + my $new = $class->new( $self->CurrentUser ); + $new->Load($args{Value}); + + return (0, $self->loc("[_1] [_2] does not exist", $self->loc($column), $args{Value})) + unless $new->id; + + my $name = eval { $current->Name } || $current->id; + + return (0, $self->loc("[_1] [_2] is disabled", $self->loc($column), $name)) + if $new->Disabled; + + return (0, $self->loc("[_1] is already set to [_2]", $self->loc($column), $name)) + if $new->id == $current->id; + + return (0, $self->loc("Permission Denied")) + if $args{RequireRight} and not $self->CurrentUser->HasRight( + Right => $args{RequireRight}, + Object => $new, + ); + + my $new_status; + my $old_lifecycle = $current->LifecycleObj; + my $new_lifecycle = $new->LifecycleObj; + if ( $old_lifecycle->Name ne $new_lifecycle->Name ) { + unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) { + return ( 0, $self->loc("There is no mapping for statuses between lifecycle [_1] and [_2]. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) ); + } + $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status }; + return ( 0, $self->loc("Mapping between lifecycle [_1] and [_2] is incomplete. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) ) + unless $new_status; + } + + my ($ok, $msg) = $self->_Set( Field => $column, Value => $new->id ); + if ($ok) { + if ( $new_status and $new_status ne $self->Status ) { + my $as_system = blessed($self)->new( RT->SystemUser ); + $as_system->Load( $self->Id ); + unless ( $as_system->Id ) { + return ( 0, $self->loc("Couldn't load copy of [_1] #[_2]", blessed($self), $self->Id) ); + } + + my ($val, $msg) = $as_system->_SetStatus( + Lifecycle => $old_lifecycle, + NewLifecycle => $new_lifecycle, + Status => $new_status, + ); + + if ($val) { + # Pick up the change made by the clone above + $self->Load( $self->id ); + } else { + RT->Logger->error("Status change to $new_status failed on $column change: $msg"); + undef $new_status; + } + } + return (1, $msg, $new_status); + } else { + return (0, $msg); + } +} + +1; |