budibase/packages/server/src/utilities/security/accessLevels.js

151 lines
4.9 KiB
JavaScript

const CouchDB = require("../../db")
const { cloneDeep } = require("lodash/fp")
const BUILTIN_IDS = {
ADMIN: "ADMIN",
POWER: "POWER_USER",
BASIC: "BASIC",
PUBLIC: "PUBLIC",
BUILDER: "BUILDER",
}
function AccessLevel(id, name, inherits) {
this._id = id
this.name = name
if (inherits) {
this.inherits = inherits
}
}
exports.BUILTIN_LEVELS = {
ADMIN: new AccessLevel(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER),
POWER: new AccessLevel(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC),
BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.PUBLIC),
PUBLIC: new AccessLevel(BUILTIN_IDS.PUBLIC, "Public"),
BUILDER: new AccessLevel(BUILTIN_IDS.BUILDER, "Builder"),
}
exports.BUILTIN_LEVEL_ID_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
level => level._id
)
exports.BUILTIN_LEVEL_NAME_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
level => level.name
)
function isBuiltin(accessLevel) {
return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1
}
/**
* Gets the access level object, this is mainly useful for two purposes, to check if the level exists and
* to check if the access level inherits any others.
* @param {string} appId The app in which to look for the access level.
* @param {string|null} accessLevelId The level ID to lookup.
* @returns {Promise<AccessLevel|object|null>} The access level object, which may contain an "inherits" property.
*/
exports.getAccessLevel = async (appId, accessLevelId) => {
if (!accessLevelId) {
return null
}
let accessLevel
if (isBuiltin(accessLevelId)) {
accessLevel = cloneDeep(
Object.values(exports.BUILTIN_LEVELS).find(
level => level._id === accessLevelId
)
)
} else {
const db = new CouchDB(appId)
accessLevel = await db.get(accessLevelId)
}
return accessLevel
}
/**
* Returns an ordered array of the user's inherited access level IDs, this can be used
* to determine if a user can access something that requires a specific access level.
* @param {string} appId The ID of the application from which access levels should be obtained.
* @param {string} userAccessLevelId The user's access level, this can be found in their access token.
* @returns {Promise<string[]>} returns an ordered array of the access levels, with the first being their
* highest level of access and the last being the lowest level.
*/
exports.getUserAccessLevelHierarchy = async (appId, userAccessLevelId) => {
// special case, if they don't have a level then they are a public user
if (!userAccessLevelId) {
return [BUILTIN_IDS.PUBLIC]
}
let accessLevelIds = [userAccessLevelId]
let userAccess = await exports.getAccessLevel(appId, userAccessLevelId)
// check if inherited makes it possible
while (
userAccess &&
userAccess.inherits &&
accessLevelIds.indexOf(userAccess.inherits) === -1
) {
accessLevelIds.push(userAccess.inherits)
// go to get the inherited incase it inherits anything
userAccess = await exports.getAccessLevel(appId, userAccess.inherits)
}
// add the user's actual level at the end (not at start as that stops iteration
return accessLevelIds
}
class AccessController {
constructor(appId) {
this.appId = appId
this.userHierarchies = {}
}
async hasAccess(tryingAccessLevelId, userAccessLevelId) {
// special cases, the screen has no access level, the access levels are the same or the user
// is currently in the builder
if (
tryingAccessLevelId == null ||
tryingAccessLevelId === "" ||
tryingAccessLevelId === userAccessLevelId ||
userAccessLevelId === BUILTIN_IDS.BUILDER
) {
return true
}
let accessLevelIds = this.userHierarchies[userAccessLevelId]
if (!accessLevelIds) {
accessLevelIds = await exports.getUserAccessLevelHierarchy(
this.appId,
userAccessLevelId
)
this.userHierarchies[userAccessLevelId] = userAccessLevelId
}
return accessLevelIds.indexOf(tryingAccessLevelId) !== -1
}
async checkScreensAccess(screens, userAccessLevelId) {
let accessibleScreens = []
// don't want to handle this with Promise.all as this would mean all custom access levels would be
// retrieved at same time, it is likely a custom levels 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, userAccessLevelId)
if (accessible) {
accessibleScreens.push(accessible)
}
}
return accessibleScreens
}
async checkScreenAccess(screen, userAccessLevelId) {
const accessLevelId =
screen && screen.routing ? screen.routing.accessLevelId : null
if (await this.hasAccess(accessLevelId, userAccessLevelId)) {
return screen
}
return null
}
}
exports.AccessController = AccessController
exports.BUILTIN_LEVEL_IDS = BUILTIN_IDS
exports.isBuiltin = isBuiltin
exports.AccessLevel = AccessLevel