const { getDB } = require("../db") const { cloneDeep } = require("lodash/fp") const { BUILTIN_PERMISSION_IDS, higherPermission } = require("./permissions") const { generateRoleID, getRoleParams, DocumentTypes, SEPARATOR, } = require("../db/utils") const BUILTIN_IDS = { ADMIN: "ADMIN", POWER: "POWER", BASIC: "BASIC", PUBLIC: "PUBLIC", } // exclude internal roles like builder const EXTERNAL_BUILTIN_ROLE_IDS = [ BUILTIN_IDS.ADMIN, BUILTIN_IDS.POWER, BUILTIN_IDS.BASIC, BUILTIN_IDS.PUBLIC, ] function Role(id, name) { this._id = id this.name = name } Role.prototype.addPermission = function (permissionId) { this.permissionId = permissionId return this } Role.prototype.addInheritance = function (inherits) { this.inherits = inherits return this } const BUILTIN_ROLES = { 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.PUBLIC ), BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission( BUILTIN_PERMISSION_IDS.ADMIN ), } exports.getBuiltinRoles = () => { return cloneDeep(BUILTIN_ROLES) } exports.BUILTIN_ROLE_ID_ARRAY = Object.values(BUILTIN_ROLES).map( role => role._id ) exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(BUILTIN_ROLES).map( role => role.name ) function isBuiltin(role) { return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin)) } /** * Works through the inheritance ranks to see how far up the builtin stack this ID is. */ function builtinRoleToNumber(id) { const builtins = exports.getBuiltinRoles() const MAX = Object.values(BUILTIN_IDS).length + 1 if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { return MAX } let role = builtins[id], count = 0 do { if (!role) { break } role = builtins[role.inherits] count++ } while (role !== null) return count } /** * Returns whichever builtin roleID is lower. */ exports.lowerBuiltinRoleID = (roleId1, roleId2) => { if (!roleId1) { return roleId2 } if (!roleId2) { return roleId1 } return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2) ? roleId2 : roleId1 } /** * 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} The role object, which may contain an "inherits" property. */ exports.getRole = async (appId, roleId) => { if (!roleId) { return null } 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) if (isBuiltin(roleId)) { role = cloneDeep( Object.values(BUILTIN_ROLES).find(role => role._id === roleId) ) } try { const db = getDB(appId) const dbRole = await db.get(exports.getDBRoleID(roleId)) role = Object.assign(role, dbRole) // finalise the ID role._id = exports.getExternalRoleID(role._id) } catch (err) { // only throw an error if there is no role at all if (Object.keys(role).length === 0) { throw err } } return role } /** * 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 } /** * 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} 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 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. * @returns {Promise<{basePermissions: string[], permissions: Object}>} the base * permission IDs as well as any custom resource permissions. */ exports.getUserPermissions = async (appId, userRoleId) => { const rolesHierarchy = await getAllUserRoles(appId, userRoleId) const basePermissions = [ ...new Set(rolesHierarchy.map(role => role.permissionId)), ] 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, } } /** * Given an app ID this will retrieve all of the roles that are currently within that app. * @param {string} appId The ID of the app to retrieve the roles from. * @return {Promise} An array of the role objects that were found. */ exports.getAllRoles = async appId => { const db = getDB(appId) const body = await db.allDocs( getRoleParams(null, { include_docs: true, }) ) let roles = body.rows.map(row => row.doc) const builtinRoles = exports.getBuiltinRoles() // need to combine builtin with any DB record of them (for sake of permissions) for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) { const builtinRole = builtinRoles[builtinRoleId] const dbBuiltin = roles.filter( dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId )[0] if (dbBuiltin == null) { roles.push(builtinRole) } else { // remove role and all back after combining with the builtin roles = roles.filter(role => role._id !== dbBuiltin._id) dbBuiltin._id = exports.getExternalRoleID(dbBuiltin._id) roles.push(Object.assign(builtinRole, dbBuiltin)) } } return roles } 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 || userRoleId === BUILTIN_IDS.BUILDER ) { return true } let roleIds = this.userHierarchies[userRoleId] if (!roleIds) { roleIds = await exports.getUserRoleHierarchy(this.appId, userRoleId) this.userHierarchies[userRoleId] = roleIds } 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 } } /** * 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 } exports.AccessController = AccessController exports.BUILTIN_ROLE_IDS = BUILTIN_IDS exports.isBuiltin = isBuiltin exports.Role = Role