DEV Community

Xabi Errotabehere for AWS Community Builders

Posted on • Updated on

I've created a full-stack user permission model, should I go open-source?

I have been working on the Hivelight application for several months, managing user roles and permissions. Initially, I used a simple approach to handle roles such as Owner, Admin, User, and Guest, each triggering a specific piece of code. This method worked well until I had to deal with more complex permissions. That's when I searched for an elegant solution to handle roles and permissions both in the back-end and front-end.

The solution needed to take into account organizations that have multiple workspaces, which in turn have multiple matters and multiple tasks. There are other relationships to take into account, but I'll keep them out of this article for brevity.

The goal is to design a fine-tuned permission model that not only considers the user's role and the resource being accessed but also the context and provides access if certain conditions are met. The solution should consider three main concepts:

The solution has to take into account 3 main concepts:

  • A resource
  • One or more actions to be performed against it
  • Whether these actions are permitted or not

The solution

I was inspired by the AWS IAM policies and came up with the HRL (Hivelight Resource Locator) concept to describe a resource or its path in the resource hierarchy.

For example, a task located in organizationId/workspaceId/matterId/taskId would translate to hrl:organizationId:workspaceId:matter:matterId:task:taskId.

To describe all tasks within a workspace, the HRL would be hrl:organizationId:workspaceId:matter:*:task:*, and to describe all matters in a workspace, it would be hrl:organizationId:workspaceId:matter:*.

Next, the solution needs to define the actions a policy holder can perform against the identified resource. These actions should consist of an action verb (create, read, update, delete) and a resource name (Task, TaskAssignee, Matter, MatterTag, etc.).

By combining these three concepts, the solution would look like a policy document like this:

    - resource: hrl:[organizationId]:[workspaceId]:matter:*
      actions:
        - updateMatterStatusMessage
        - createMatterTag
        - createMatterNote
      effect: allow
Enter fullscreen mode Exit fullscreen mode

This allows a policy holder to update a matter status message, create a matter tag, and create a matter note on any matter in a given workspace.

Multiple statements can be combined to create a rich and fine-tuned permission model, like this:

    - resource: hrl:[organizationId]:[workspaceId]:matter:*
      actions: “*”
      effect: deny
    - resource: hrl:[organizationId]:[workspaceId]:matter:[matterId]
      actions:
        - readMatter
      effect: allow
Enter fullscreen mode Exit fullscreen mode

This allows a policy holder to read a specific matterId in a workspace but deny access to any other matter

The library

The solution includes a full-stack library to handle actions and determine if a specific action can be taken against a specific resource. The code for this would look like this:

permission.can("actionResource")
Enter fullscreen mode Exit fullscreen mode

This returns true or false depending on the permission.

For example:

<div>
  {permission.can("updateMatterStatusMessage") ? (
    <StatusUpdate matterId={matter.id} />
  ) : undefined}
</div>
Enter fullscreen mode Exit fullscreen mode

or

<div>
  {permission.cannot("createMatter") ? (
    <p>You don't have enough privilege to create a matter</p>
  ) : undefined}
</div>
Enter fullscreen mode Exit fullscreen mode

or like that in the backend

if (permission.cannot("createMatter")) {
  throw new Error("Forbidden")
}
Enter fullscreen mode Exit fullscreen mode

To provide context for the logic that makes the "can/cannot" decision, we need to construct an HRL that describes the resource being dealt with, the policy or policies held by the current user, and the context of who the current user is and what other entities are involved.

Consider this:

// constructing the hrl
const hrl = [
  "hrl", 
  currentOrganizationId, // id is 123
  currentWorkspaceId // id is ABC
].join(":");

// constructing the context object
const context = {
   currentUser
}

// creating the permission object assuming
// the user object contains the policy document
const permission = new Permission(hrl, currentUser.policy, context);

// ... then somewhere in the code
if (permission.cannot("updateMatterStatusMessage")) {
  throw new Error("Forbidden")
}

Enter fullscreen mode Exit fullscreen mode

In the example, the HRL is constructed as hrl:123:ABC, where 123 and ABC are the IDs of the current organization and workspace, respectively. The context object contains information about the current user, and this information, along with the user's policy, is passed to create a permission object.

Imagine the policy has the following statements:

- name: WorkspaceMember
  statements:
    - resource: hrl:123:ABC
      actions: "*"
      effect: allow
    - resource: hrl:123:ABC:matter:*
      actions: "*"
      effect: allow
Enter fullscreen mode Exit fullscreen mode

The code above will not throw an error because this user can perform any action actions: "*" on every matter resource: hrl:123:ABC:matter:*

Now, imagine the policy has the following statements:

- name: WorkspaceMember
  statements:
    - resource: hrl:123:ABC
      actions: "*"
      effect: allow
    - resource: hrl:123:ABC:matter:*
      actions: "*"
      effect: allow
    - resource: hrl:123:ABC:matter:*
      actions: 
        - updateMatterStatusMessage
        - deleteMatter
      effect: deny
Enter fullscreen mode Exit fullscreen mode

The code above would throw an error because the user is denied updateMatterStatusMessage action on every matter

The library mentioned here is lightweight and has been used successfully in production for both the frontend and backend.
It comes with more features not developed here, such as conditional permissions.

It has the advantage of keeping the front and backend logic unchanged when the permission model changes and allowing roles and permission models to be applied on a per-user basis, with each user holding a policy that suits them.

Another big advantage is also that the logic and the policy are kept separate. This enables the usage of different libraries (i.e. Java back-end and JS front-end) to use the same policy documents.

I am considering making the library open-source. If you have used similar libraries before, what are their shortcomings, and do you think the model described here can solve them? Would you be interested in taking a look at the library? What role and permission library do you use?

Top comments (0)