Skip to main content

Security, Roles & Permissions

Overview

We are using JSON Web Token (JWT) for authorisation and use scopes (permissions) to control access to endpoints.

We are using AWS Cognito as our user directory and authentication resource.

In combination with openapi-backend, this makes handling permissions for the backend API endpoints a simple process of adding the permissions scopes to the endpoint and ensuring that all calls made to that endpoint satisfy those constraints.

Securing your endpoint

We are employing a deny/allow security policy for our endpoints, as the system is accessible mostly to logged-in users, by default all endpoints must be restricted to logged-in users which is achieved by adding

security: [
{ jwt: [] }
]

to the OpenAPIV3.Document this is stored in /modules/<module>/openapi/document.ts. From there, amend each path and add the required scopes to the path if stricter permissions are desired.

If your path does need to be open to non-logged-in users, then add security:[] to the path object

Modules, Roles & Permissions

System access is controlled via the roles component here components/common/roles.ts. Access is managed via modules, roles and permissions...

  • Module An organisation can have access to one or more modules in the system and a module can be either gated (paid for add-on) or included (part of the organisations core monthly cost).
  • Role A role grants access to a set of permissions, for example a user may be granted the inv-rec-read role which grants them read access to inventory records.
  • Permission A permission is the lowest level of access to the system and is what we add to endpoints to control access, for example inv:rec:r is the permission needed by a user to access the Inventory Record GET endpoints.

Modules

A module's settings comprise of the following...

{
id: 'inventory:core',
name: 'Inventory: Core',
roleGroups: ['storage', 'movements', 'adjustments', 'contracts', 'business', 'teams'],
manager: {
role: 'inv-manage',
permissions: ['inv:*:w', 'cus:*:w']
},
autoAccess: false,
hideAccess: ['trading.inventory.read', 'trading.inventory.write']
}
  • id is stored on organisations in the database to indicate which modules the organosation has access to
  • roleGroups are a list of the role groups that are applicable to this module, a roleGroup is a set of roles, the purpose of this is to allow us to know which roles & therefore permissions are relevant to each module
  • manager each module has a manager role, a manager role has full access to the module via the permission set shown above. You will note that the inventory manager also has access to the customer (cus) and system (sys) modules.
  • autoAccess if true users whose organisation has this gated module automatically have access, otherwise they need the roles explicitly assigned
  • hideAccess hide these front end access keys if this module is enabled

Role Groups

A role group defines a set of roles and UI access...

{
id: 'inventory-records',
name: 'Inventory: Records',
hidden: false,
roles: [
{
id: null,
name: 'No Access',
permissions: []
},
{
id: 'inv-rec-read',
name: 'View Only',
permissions: ['inv:rec:r'],
access: ['inventory.records.read']
},
{
id: 'inv-rec-write',
name: 'Create & Edit',
permissions: ['inv:rec:w'],
access: ['inventory.records.read', 'inventory.records.write'],
manager: true
}
]
}
  • id and name are self explanatory
  • hidden if true this role group is not visible to the front end (permission UI for assigning roles to users)
  • roles array of roles applicable to this group, this is used in the front end on the permission UI drop down for each role group to assign the level of access a user has to a given group of roles which usually relates to an area of the system.

Roles

A role is made up of the following

{
id: 'inv-rec-write',
name: 'Create & Edit',
permissions: ['inv:rec:w'],
access: ['inventory.records.read', 'inventory.records.write'],
manager: true
}
  • id this is the stored on the user object in RavenDB and in the users Cognito account (and in their tokens)
  • permissions array of specific permissions that grant access to endpoints
  • access array of access values which we send to the front end in the GET /customers/me endpoint, the front end uses the array of access parameters to control which menu items / CTAs etc are available to the user on the front end, so effectively the users roles control their front end access.
  • manager used when we need to build an array of specific roles a manager role grants a user access to, see determineUserRoles function in components/common/roles.ts

Authorising an Endpoint

Endpoint authorisation is handled via the security handler here components/api/handlers/security.ts.

This component...

  1. Decodes the users JWT token
  2. Extracts users roles from their Cognito account and builds a list of permissions the roles grants the user access to.
  3. Builds a set of regular expressions based on the users permissions and attempts to find matches on the endpoints permissions
  4. If a match is found the user has access otherwise an exception is thrown.

Permission structure

A permission is made up of 3 parts module:area:access for example inv:rec:r is inventory module, records area and read access.

The 3rd part (access) has 3 possible values

  • r read
  • w write
  • a admin

If a user has admin access they automatically get read and write, if a user has write access they automatically get read, this is managed via the regex matching in the security handler

A permission can contain wildcards to grant access to broad areas of the system, for example inv:*:r gives the user read access to all of the inventory module.

The system admin user has access to all endpoints and has one permission *:*:a

Other points

  1. Because we're storing roles on a users Cognito account not permissions we can easily change access to the system without impacting the user accounts in Cognito, this should allow us to make changes without needing to run migrations of user acocunt roles in Cognito
  2. If a user is granted new roles (or has roles removed) their access wont be updated until they login again, this is because their access in the APIs is granted via their access & id tokens. Because of this we force log them out when their account permissions change using the cognito.admin.signOut() function, note however that this only invalidates the users refresh token so they will remain without the permission changes until their access token expires (180 minutes) or they login again