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
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
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")
This returns true or false depending on the permission.
For example:
<div>
{permission.can("updateMatterStatusMessage") ? (
<StatusUpdate matterId={matter.id} />
) : undefined}
</div>
or
<div>
{permission.cannot("createMatter") ? (
<p>You don't have enough privilege to create a matter</p>
) : undefined}
</div>
or like that in the backend
if (permission.cannot("createMatter")) {
throw new Error("Forbidden")
}
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")
}
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
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
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)