-
Notifications
You must be signed in to change notification settings - Fork 7
Roles and permissions
Original presentation - Slide deck for communicating the original architecture design.
The assignment and enforcement of permissions throughout the Aperta application are critical to prevent fraud and protect anonymity. The implementation goal is to support enforcement of permissions on both the backend and front-end of Aperta in a consistent, generic, and maintainable manner.
The Aperta backend is the authority on access control for the system. A simple example is that it prevents a user from viewing a task on a paper that they do not have the ability to access.
The Aperta front-end is responsible in presenting contextually relevant information and functionality to the user. A simple example is not to show a button on the screen that the user would not be allowed to click.
These business goals coupled with a the desire for Aperta to become increasingly more data driven has resulted in a very flexible, reliable system, but also means it operates with a certain level of complexity. This document is designed to reduce the mystery of how authorization works within the system.
There are countless well tested, community supported gems that provide authorization mechanisms out of the box, so why develop a custom solution for Aperta?
Two of the biggest requirements for Aperta are: 1. Apply and enforce
different Roles
per Journal
instance 2. Apply different permissions
depending on the state of the Paper
Due to the complex nature of the business requirements, it was decided that a custom solution was necessary.
There are several models involved with granting and enforcing permissions. They provide the domain language for further discussion and understanding throughout this document.
-
User
- the person who is granted some level of access to a Thing -
Thing (
Journal
,Paper
,Task
) - the model that is being
protected by access controls -
Role
- a human understandable name that represents a user's part in the publishing process. It indicates what a user is interested in as opposed to what they have access to. AUser
can have multipleRoles
-
Permission
- the most granular unit of access that Aperta can enforce. It is a combination of an action describing what the permission is and what it applies to. It says what access is being granted to a Thing. -
State
- answers when aPermission
can be applied. Defining a state is optional and is used only in specific cases where a more granular level of control is necessary. Example:user
is only granted access to a particulartask
when the paper state is submitted. -
Assignment
- connects aUser
, a Thing, and aRole
together. It provides the context of how a user got access to a Thing and is intended to be the single source of truth for how any kind of access is granted within Aperta. Finally, it provides the scope that maintains the hierarchial nature of the Things being controlled.
An Assignment
has a triplet of User
, Thing, and Role
which defines
how a user gets access access in the system. A Permission
has its
own triplet consisting of Role
, Journal
, State
which defines
what a user can access in the system.
Lucy is assigned to PLOS Bio Journal (Assignment
) as an Internal
Editor (Role
) where the role has the following Permissions
:
- view access to
Journal
- view access to
Paper
- view access to
Task
This means that Lucy can view the PLOS Bio journal, view any paper within the PLOS Bio journal, and view any task within the PLOS Bio Journal.
Bob is assigned to Paper 1 (Assignment
) as an Author (Role
) where
the role has the following Permission
:
- view access to
Paper
This means that Bob can view Paper 1. He cannot access any other papers in the system.
Karen is assigned to Reviewer Report Task (Assignment
) on Paper 1 as a
Reviewer (Role
) where the role has the following Permissions
:
- view access to Task
- view access on Paper
This means that Karen can view the Reviewer Report Task on Paper 1 and view Paper 1.
The Assignment
provides the context that the permissions are applied,
so the "view access on Paper" Permission
is scoped just to Paper 1.
Bruce is assigned to Reviewer Report Task (Assignment
) on Paper 1 as a
Reviewer (Role
) where the role has the following Permissions
:
- view access to Task
- view access on Paper with
State
of "submitted"
This means that Bruce can view the Reviewer Report Task on Paper 1 and view Paper 1, but only when the paper is in a "submitted" state.
When a User
is assigned to a specific Journal
, the system often
needs to report what Papers
the user has access to. Therefore, the
system somehow needs to know to call the papers
collection proxy on
the Journal
class.
The plumbing for this logic is handled by the initializer:
config/initializers/z_authorizations.rb
where the configuration logic
looks like Listing assignment .
Authorizations.configure do |config|
config.assignment_to(
Journal,
authorizes: Paper,
via: :papers
)
end
In this example, the hierarchal mapping of having access to Papers
when having Journal
access through Journal#papers
method is stored
in memory and used when needed.
When working with assignment relationship and making authorization
queries, there is often an participations_only
option that can be
specified.
This option is used to check if any of the roles assigned to a user
should be considered as an active participant as opposed to somebody
with access. This is useful for acting as a filter when displaying
relevant papers on a user's dashboard.
Some users, like Journal-level Staff Admins, have access to a lot of papers in the system, but they aren't participating in many of them. Maybe even none of them. When trying to determine what to show on the dashboard it may be relevant to display papers they are actively participating in.
Role
records are simply a searchable string name that have
Permissions
assigned to them. All of the Role
names are found as
constants in app/models/role.rb
.
- Journal Roles
- Freelance Editor
- Internal Editor
- Production Staff
- Publishing Services
- Staff Admin
- Journal Setup
- Paper Roles
- Academic Editor
- Collaborator
- Cover Editor
- Creator
- Handling Editor
- Reviewer
- Task Roles
- Reviewer Report Owner
- Task Participant
Permissions
are assigned to the role in
app/services/journal_factory.rb
This service class is also responsible
for ensuring that the role exists on the Journal
.
Listing creationassignment is an example of this creation and assignment.
Role.ensure_exists(Role::JOURNAL_SETUP_ROLE, journal: @journal) do |role|
role.ensure_permission_exists(:administer, applies_to: Journal)
end
This finds or creates the Role
and associated Permission
for the
specified Journal
.
A CustomCardTask
is a data driven task model that can display
questions and content configured by an Aperta site administrator. While
permissions for legacy tasks are defined explicitly in code, permissions
for CustomCardTasks
are exposed to the user interface as a series of
checkboxes in the card administration screen. Figure
cardadmin is a screenshot of these settings.
This presents an interesting problem. Two instances of a
CustomCardTask
might be used on a single Journal
to ask the user two
different sets of questions and each of those CustomCardTask
may need
to have different permissions applied. The system needs a way to
differentiate the permissions between CustomCardTasks
. Behind the
scenes, the system still uses the same Role
, Assignment
, and
Permission
models, but with one difference: an attribute called
filter_by_card_id
is leveraged on the Permission
model.
This attribute uses a foreign key relation to Card
to distinguish
permissions that are backed by differing instances of CustomCardTasks
.
In fact, the presence of this field is enforced through ActiveRecord
validations.
Aperta leverages this relationship when making roles and permission queries which are detailed in the following section.
Permissions work on the frontend and the backend parts of Aperta. Examples of these usages follow.
Some helper methods exist to verify the permissions of a User
for a
Thing. They follow a similar syntax to the cancan gem, but perform
efficient database calls instead. These can be used anywhere, but most
commonly they are leveraged in controllers to prevent unauthorized user
access or accidentally returning protected data. The code responsible
for managing and querying roles and permissions are found here:
app/models/authorizations/
This is the simplest query which returns a
boolean indicating whether the user has access to the paper:
@user.can?(:view, @paper)
It is also possible to filter a list of Things down to just ones that a
User
has access to. This is helpful when returning a list of Things to
display to the User
. This is smart enough to generate an
ActiveRecord
scope so that it can be further chained.
@user.filter_authorized(:view, Paper.all)
To find all the users who have access to a particular Thing, a reverse
query can be used. However, please note that States
are not taken into
account with this type of query:
Authorizations::ReverseQuery.new(permission: :view, target: @paper)
Ember helper functions also exist to prevent actions or data being shown that the user does not have the ability to use. The ember-can service is ready to be used in components, templates, or routes and uses the same permission data from the server to make the permission determination. Example of view template usage:
{{#if (can "edit" paper)}}
<a class="contributors-add" {{action "addContributors"}} id="nav-add-collaborators">Add Collaborators</a>
{{/if}}
Figure canservice illustrates how the can service works.
- The
can
helper asks thecan
service to build an Ability based on the name of the action and the resource ('edit', and a paper in this case) - The 'can' service looks for a
Permission
record in the store based on the name of the resource. If it does not find one locally, it makes a request to the Rails APIPermissionsController
- The
PermissionsController
uses the Roles and Permissions system to build up a list of things the current user can do for the given resource, and returns it as a Permission record to the client. - Once the
Permission
is returned, it is cached in the store so that it does not need to make a new request for eachcan
check - The
can
service returns the new Ability with its permissions set.
We have recently introduced a new concept: permission filters. These are configurable additional filters that must be fulfilled for a permission to apply. The first use of these is card based filters, but they could eventually replace the existing state and STI (single table inheritance filters). These configurable filters tie together an AREL query with an additional column on the permissions table to provide criterion that must match in order for the permission to apply.
As an example, say we wanted to implement a filter that the permission applied only to tasks that are not "completed".
As a first step, we would add a new required_task_incomplete
boolean
column to the permissions table. This would be set to true
if the
permission applied only to incomplete tasks, or false
otherwise.
config.filter(Task, :required_task_incomplete) do |query, column, table|
query.where(column.eq(false).or(table.completed.eq(false)))
end
A configuration block is passed three parameters: the query that is
currently being constructed to find permissions, the column on the
permissions table (and its value), in this case
permissions.required_task_incomplete
, and the table for the targeted
permission object (in this case tasks
).
This system should allow a sufficient level of flexibility to replace some existing custom filtering, and provide an entry point for other kinds of filtering we may need to provide in the future.
- https://confluence.plos.org/confluence/display/TAHI/Roles+and+Permissions
- https://confluence.plos.org/confluence/display/TAHI/Client+Side+Authorization+Workflow
cardadmin.png (image/png)
randp.png (image/png)
canservice.png (image/png)