IMA-WorldHealth / bhima

A hospital information management application for rural Congolese hospitals
GNU General Public License v2.0
218 stars 104 forks source link

Design: Roles and Permissions #2665

Open jniles opened 6 years ago

jniles commented 6 years ago

This issue concerns the overall design and implementation of roles and permissions in 2.x. Before reading, it would be worth taking a look at some of the modules other people are using for permissions, roles, and access control lists.

Here you go: https://gist.github.com/facultymatt/6370903

NOTE: I will call the basic unit of permissions here a "token", but this name is only for clarity. We can call it whatever we want in the future

Implementation of Access Control

I propose that we implement access control in 2.x as a series of tokens for each route. This means, for each app.get()/app.put()/etc, we will need at least one token. In pseudo-code, it would look like this:

app.get('/users', access.token('users:list'), users.list);
app.put('/users/:id', access.token('users:update'), users.update);
app.get('/users/:id/roles', access.token('users:roles'), users.getRolesByUser);
app.post('/users', access.token('users:create'), users.create);

In the above example, the access.token() middleware would use the req.session object to make sure that the user had access to that route. If they did not have access, a 401 Unauthorized request would be returned.

The node access control that most closely resembles this structure is authorized.

Assigning Access Control to Users

We landed a PR that ties users to roles and units. The basic philosophy here is to make a finer-grained permissions model than units - we should be able to control access to each individual piece of information. Roles would then be assigned to these pieces.

We would need to maintain a list of tokens in the database. A table would look like this:

INSERT INTO `token` (id, label, parent) VALUES 
  (1, 'users', NULL),
  (2, 'users:list', 1),
  (3, 'users:create',1);

Note that parent here is just to allow a user to assign groups of tokens more easily through a tree interface, like we do for units now.

Roles are simply groups of tokens. Users can hold one or more roles. Every user must have a role.

Using Tokens on the Client for Access Control

Important: Before doing any work on Access Control on the client, we must make sure that the server works correctly and consistently. Remember, you cannot trust the client to be secure. All permissions must be enforced at the server first and then on the client.

In the same way that we will use tokens on the server, we can label different elements of the client with similar tokens (if not the same). Below will go through some examples.

Using Tokens to Show/Hide HTML

We can write a custom directive that basically performs the same operation as ngIf, but shows/hides based on the user's access level. For example, look at the patient record page below:

patientrecordpage

You can image decomposing this into the following sections (they are currently implemented as ui-views).

<bh-patient-overview></bh-patient-overview>

<div class="row">
  <div class="col-xs-6">
    <bh-patient-documents></bh-patient-documents>
  </div>
  <div class="col-xs-6">
    <bh-patient-visits></bh-patient-visits>
  </div>
</div>

<bh-patient-activity></bh-patient-activity>

This architecture is now easy to put permissions on top of. All we need to do is define a directive that shows/hides HTML based on if they have access to the module. For example:

<bh-patient-overview bh-access="patients:record:overview"></bh-patient-overview>

<div class="row">
  <div class="col-xs-6">
    <bh-patient-documents bh-access="patients:record:documents"></bh-patient-documents>
  </div>
  <div class="col-xs-6">
    <bh-patient-visits bh-access="patients:record:visits"></bh-patient-visits>
  </div>
</div>

<bh-patient-activity bh-access="patients:record:activity"></bh-patient-activity>

A user who only has access to patient:record:overview will only see the top box. A user who has access to patient:record:* will see all of them. And so on.

Using Tokens to Enable/Disable Links

We can do a similar thing with <a href></a> links. Often, we want to preserve the link, since it has useful information, but we don't want the user to be able to click it.

For example, imagine a user who has access to the Debtor Groups Management page. The page looks like this: debtorgroups

The user who has access to manage the debtor groups many not have the right to see the patients linked through the X Subscribed Debtors link.

We could limit their access by creating a directive that adds a dynamic class to the link, disabling it and coloring it grey (to indicate that they do not have access). This would look something like this:

<a href ui-sref="patientRegistry({ filters : [{ debtor_uuid : XYZ }]})" bh-access-link="patients:list" translate></a>

The bhAccessLink would then check if the user (using the SessionService) had access to the link, and, if not, apply the class .bh-access-disabled which would do something like:

.bh-access-disabled {
  pointer-events : none;
  color: #333;
  cursor: disabled;
}

Using Tokens to Prevent Routing/State Changes

Finally, one of the last places we could put tokens to disable access is on the routes. This would replace our custom $stateChange events with a more consistent approach.

In the state definition we would put the token as a parameter that uses a state transition hook to allow or reject transition.


Useful Links:

  1. Github's approach to permissions.
jniles commented 4 years ago

This is relevant: https://www.npmtrends.com/accesscontrol-vs-acl-vs-express-acl-vs-express-authorization-vs-node-authorization-vs-@casl/mongoose-vs-@casl/ability