2020-12-02 14:20:56 +01:00
|
|
|
const CouchDB = require("../../db")
|
|
|
|
const { cloneDeep } = require("lodash/fp")
|
2021-02-09 14:01:45 +01:00
|
|
|
const { BUILTIN_PERMISSION_IDS, higherPermission } = require("./permissions")
|
|
|
|
const { generateRoleID, DocumentTypes, SEPARATOR } = require("../../db/utils")
|
2020-12-02 14:20:56 +01:00
|
|
|
|
|
|
|
const BUILTIN_IDS = {
|
|
|
|
ADMIN: "ADMIN",
|
2020-12-08 13:20:37 +01:00
|
|
|
POWER: "POWER",
|
2020-12-02 14:20:56 +01:00
|
|
|
BASIC: "BASIC",
|
|
|
|
PUBLIC: "PUBLIC",
|
|
|
|
BUILDER: "BUILDER",
|
|
|
|
}
|
|
|
|
|
2020-12-02 18:08:25 +01:00
|
|
|
function Role(id, name) {
|
2020-12-02 14:20:56 +01:00
|
|
|
this._id = id
|
|
|
|
this.name = name
|
2020-12-02 18:08:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Role.prototype.addPermission = function(permissionId) {
|
|
|
|
this.permissionId = permissionId
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
Role.prototype.addInheritance = function(inherits) {
|
|
|
|
this.inherits = inherits
|
|
|
|
return this
|
2020-12-02 14:20:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
exports.BUILTIN_ROLES = {
|
2020-12-02 18:08:25 +01:00
|
|
|
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin")
|
|
|
|
.addPermission(BUILTIN_PERMISSION_IDS.ADMIN)
|
|
|
|
.addInheritance(BUILTIN_IDS.POWER),
|
|
|
|
POWER: new Role(BUILTIN_IDS.POWER, "Power")
|
|
|
|
.addPermission(BUILTIN_PERMISSION_IDS.POWER)
|
|
|
|
.addInheritance(BUILTIN_IDS.BASIC),
|
|
|
|
BASIC: new Role(BUILTIN_IDS.BASIC, "Basic")
|
|
|
|
.addPermission(BUILTIN_PERMISSION_IDS.WRITE)
|
|
|
|
.addInheritance(BUILTIN_IDS.PUBLIC),
|
|
|
|
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission(
|
|
|
|
BUILTIN_PERMISSION_IDS.READ_ONLY
|
|
|
|
),
|
|
|
|
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission(
|
|
|
|
BUILTIN_PERMISSION_IDS.ADMIN
|
|
|
|
),
|
2020-12-02 14:20:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
2021-02-09 14:01:45 +01:00
|
|
|
role => role._id
|
2020-12-02 14:20:56 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
2021-02-09 14:01:45 +01:00
|
|
|
role => role.name
|
2020-12-02 14:20:56 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
function isBuiltin(role) {
|
2021-02-09 14:01:45 +01:00
|
|
|
return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin))
|
2020-12-02 14:20:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
|
|
|
* to check if the role inherits any others.
|
|
|
|
* @param {string} appId The app in which to look for the role.
|
|
|
|
* @param {string|null} roleId The level ID to lookup.
|
|
|
|
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
|
|
|
|
*/
|
|
|
|
exports.getRole = async (appId, roleId) => {
|
|
|
|
if (!roleId) {
|
|
|
|
return null
|
|
|
|
}
|
2021-02-05 16:58:25 +01:00
|
|
|
let role = {}
|
|
|
|
// built in roles mostly come from the in-code implementation,
|
|
|
|
// but can be extended by a doc stored about them (e.g. permissions)
|
2020-12-02 14:20:56 +01:00
|
|
|
if (isBuiltin(roleId)) {
|
|
|
|
role = cloneDeep(
|
|
|
|
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
|
|
|
|
)
|
2021-02-05 16:58:25 +01:00
|
|
|
}
|
|
|
|
try {
|
2020-12-02 14:20:56 +01:00
|
|
|
const db = new CouchDB(appId)
|
2021-02-05 16:58:25 +01:00
|
|
|
const dbRole = await db.get(roleId)
|
|
|
|
role = Object.assign(role, dbRole)
|
2021-02-09 14:01:45 +01:00
|
|
|
// finalise the ID
|
|
|
|
role._id = exports.getExternalRoleID(role._id)
|
2021-02-05 16:58:25 +01:00
|
|
|
} catch (err) {
|
|
|
|
// only throw an error if there is no role at all
|
|
|
|
if (Object.keys(role).length === 0) {
|
|
|
|
throw err
|
|
|
|
}
|
2020-12-02 14:20:56 +01:00
|
|
|
}
|
|
|
|
return role
|
|
|
|
}
|
|
|
|
|
2020-12-02 18:08:25 +01:00
|
|
|
/**
|
|
|
|
* Simple function to get all the roles based on the top level user role ID.
|
|
|
|
*/
|
|
|
|
async function getAllUserRoles(appId, userRoleId) {
|
|
|
|
if (!userRoleId) {
|
|
|
|
return [BUILTIN_IDS.PUBLIC]
|
|
|
|
}
|
|
|
|
let currentRole = await exports.getRole(appId, userRoleId)
|
|
|
|
let roles = currentRole ? [currentRole] : []
|
|
|
|
let roleIds = [userRoleId]
|
|
|
|
// get all the inherited roles
|
|
|
|
while (
|
|
|
|
currentRole &&
|
|
|
|
currentRole.inherits &&
|
|
|
|
roleIds.indexOf(currentRole.inherits) === -1
|
|
|
|
) {
|
|
|
|
roleIds.push(currentRole.inherits)
|
|
|
|
currentRole = await exports.getRole(appId, currentRole.inherits)
|
|
|
|
roles.push(currentRole)
|
|
|
|
}
|
|
|
|
return roles
|
|
|
|
}
|
|
|
|
|
2020-12-02 14:20:56 +01:00
|
|
|
/**
|
|
|
|
* Returns an ordered array of the user's inherited role IDs, this can be used
|
|
|
|
* to determine if a user can access something that requires a specific role.
|
|
|
|
* @param {string} appId The ID of the application from which roles should be obtained.
|
|
|
|
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
|
|
|
* @returns {Promise<string[]>} returns an ordered array of the roles, with the first being their
|
|
|
|
* highest level of access and the last being the lowest level.
|
|
|
|
*/
|
|
|
|
exports.getUserRoleHierarchy = async (appId, userRoleId) => {
|
|
|
|
// special case, if they don't have a role then they are a public user
|
2020-12-02 18:08:25 +01:00
|
|
|
return (await getAllUserRoles(appId, userRoleId)).map(role => role._id)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all of the user permissions which could be found across the role hierarchy
|
|
|
|
* @param appId The ID of the application from which roles should be obtained.
|
|
|
|
* @param userRoleId The user's role ID, this can be found in their access token.
|
2021-02-08 18:52:22 +01:00
|
|
|
* @returns {Promise<{basePermissions: string[], permissions: Object}>} the base
|
|
|
|
* permission IDs as well as any custom resource permissions.
|
2020-12-02 18:08:25 +01:00
|
|
|
*/
|
2021-02-08 18:52:22 +01:00
|
|
|
exports.getUserPermissions = async (appId, userRoleId) => {
|
|
|
|
const rolesHierarchy = await getAllUserRoles(appId, userRoleId)
|
|
|
|
const basePermissions = [
|
|
|
|
...new Set(rolesHierarchy.map(role => role.permissionId)),
|
2020-12-02 18:08:25 +01:00
|
|
|
]
|
2021-02-08 18:52:22 +01:00
|
|
|
const permissions = {}
|
|
|
|
for (let role of rolesHierarchy) {
|
|
|
|
if (role.permissions) {
|
|
|
|
for (let [resource, level] of Object.entries(role.permissions)) {
|
|
|
|
permissions[resource] = higherPermission(permissions[resource], level)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
basePermissions,
|
|
|
|
permissions,
|
|
|
|
}
|
2020-12-02 14:20:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
class AccessController {
|
|
|
|
constructor(appId) {
|
|
|
|
this.appId = appId
|
|
|
|
this.userHierarchies = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
async hasAccess(tryingRoleId, userRoleId) {
|
|
|
|
// special cases, the screen has no role, the roles are the same or the user
|
|
|
|
// is currently in the builder
|
|
|
|
if (
|
|
|
|
tryingRoleId == null ||
|
|
|
|
tryingRoleId === "" ||
|
|
|
|
tryingRoleId === userRoleId ||
|
|
|
|
tryingRoleId === BUILTIN_IDS.BUILDER
|
|
|
|
) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
let roleIds = this.userHierarchies[userRoleId]
|
|
|
|
if (!roleIds) {
|
|
|
|
roleIds = await exports.getUserRoleHierarchy(this.appId, userRoleId)
|
2020-12-04 15:01:02 +01:00
|
|
|
this.userHierarchies[userRoleId] = roleIds
|
2020-12-02 14:20:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return roleIds.indexOf(tryingRoleId) !== -1
|
|
|
|
}
|
|
|
|
|
|
|
|
async checkScreensAccess(screens, userRoleId) {
|
|
|
|
let accessibleScreens = []
|
|
|
|
// don't want to handle this with Promise.all as this would mean all custom roles would be
|
|
|
|
// retrieved at same time, it is likely a custom role will be re-used and therefore want
|
|
|
|
// to work in sync for performance save
|
|
|
|
for (let screen of screens) {
|
|
|
|
const accessible = await this.checkScreenAccess(screen, userRoleId)
|
|
|
|
if (accessible) {
|
|
|
|
accessibleScreens.push(accessible)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return accessibleScreens
|
|
|
|
}
|
|
|
|
|
|
|
|
async checkScreenAccess(screen, userRoleId) {
|
|
|
|
const roleId = screen && screen.routing ? screen.routing.roleId : null
|
|
|
|
if (await this.hasAccess(roleId, userRoleId)) {
|
|
|
|
return screen
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-09 14:01:45 +01:00
|
|
|
/**
|
|
|
|
* Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions).
|
|
|
|
*/
|
|
|
|
exports.getDBRoleID = roleId => {
|
|
|
|
if (roleId.startsWith(DocumentTypes.ROLE)) {
|
|
|
|
return roleId
|
|
|
|
}
|
|
|
|
return generateRoleID(roleId)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove the "role_" from builtin role IDs that have been written to the DB (for permissions).
|
|
|
|
*/
|
|
|
|
exports.getExternalRoleID = roleId => {
|
|
|
|
// for built in roles we want to remove the DB role ID element (role_)
|
|
|
|
if (roleId.startsWith(DocumentTypes.ROLE) && isBuiltin(roleId)) {
|
|
|
|
return roleId.split(`${DocumentTypes.ROLE}${SEPARATOR}`)[1]
|
|
|
|
}
|
|
|
|
return roleId
|
|
|
|
}
|
|
|
|
|
2020-12-02 14:20:56 +01:00
|
|
|
exports.AccessController = AccessController
|
|
|
|
exports.BUILTIN_ROLE_IDS = BUILTIN_IDS
|
|
|
|
exports.isBuiltin = isBuiltin
|
|
|
|
exports.Role = Role
|