summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Record/Role
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Record/Role')
-rw-r--r--rt/lib/RT/Record/Role/Lifecycle.pm219
-rw-r--r--rt/lib/RT/Record/Role/Links.pm174
-rw-r--r--rt/lib/RT/Record/Role/Rights.pm133
-rw-r--r--rt/lib/RT/Record/Role/Roles.pm633
-rw-r--r--rt/lib/RT/Record/Role/Status.pm314
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;