Skip to content

Roles and permissions

Erik Hetzner edited this page Apr 18, 2018 · 7 revisions

Roles and Permissons

Original presentation - Slide deck for communicating the original architecture design.

Overview

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.

Why Custom?

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.

Models and Description

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. A User can have multiple Roles
  • 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 a Permission 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 particular task when the paper state is submitted.
  • Assignment - connects a User, a Thing, and a Role 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.

Definitions of Authorization

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.

Example 1

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.

Example 2

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.

Example 3

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.

Example 4

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.

Assignment Relationships

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.

Participants Only

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.

Roles

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

Assigning Permissions to Roles

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.

Custom Card Permissions

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.

How Permissions Are Used

Permissions work on the frontend and the backend parts of Aperta. Examples of these usages follow.

Backend

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)

Front End

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.

  1. The can helper asks the can service to build an Ability based on the name of the action and the resource ('edit', and a paper in this case)
  2. 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 API PermissionsController
  3. 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.
  4. Once the Permission is returned, it is cached in the store so that it does not need to make a new request for each can check
  5. The can service returns the new Ability with its permissions set.

Permission filters

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.

References

Attachments:

cardadmin.png (image/png)

randp.png (image/png)

canservice.png (image/png)

Clone this wiki locally