forked from tsai/budibase
31 changed files with 330 additions and 363 deletions
@ -1,52 +0,0 @@ |
|||||
const CouchDB = require("../../db") |
|
||||
const { |
|
||||
BUILTIN_LEVELS, |
|
||||
AccessLevel, |
|
||||
getAccessLevel, |
|
||||
} = require("../../utilities/security/accessLevels") |
|
||||
const { |
|
||||
generateAccessLevelID, |
|
||||
getAccessLevelParams, |
|
||||
} = require("../../db/utils") |
|
||||
|
|
||||
exports.fetch = async function(ctx) { |
|
||||
const db = new CouchDB(ctx.user.appId) |
|
||||
const body = await db.allDocs( |
|
||||
getAccessLevelParams(null, { |
|
||||
include_docs: true, |
|
||||
}) |
|
||||
) |
|
||||
const customAccessLevels = body.rows.map(row => row.doc) |
|
||||
|
|
||||
const staticAccessLevels = [BUILTIN_LEVELS.ADMIN, BUILTIN_LEVELS.POWER] |
|
||||
ctx.body = [...staticAccessLevels, ...customAccessLevels] |
|
||||
} |
|
||||
|
|
||||
exports.find = async function(ctx) { |
|
||||
ctx.body = await getAccessLevel(ctx.user.appId, ctx.params.levelId) |
|
||||
} |
|
||||
|
|
||||
exports.save = async function(ctx) { |
|
||||
const db = new CouchDB(ctx.user.appId) |
|
||||
|
|
||||
let id = ctx.request.body._id || generateAccessLevelID() |
|
||||
const level = new AccessLevel( |
|
||||
id, |
|
||||
ctx.request.body.name, |
|
||||
ctx.request.body.inherits |
|
||||
) |
|
||||
if (ctx.request.body._rev) { |
|
||||
level._rev = ctx.request.body._rev |
|
||||
} |
|
||||
const result = await db.put(level) |
|
||||
level._rev = result.rev |
|
||||
ctx.body = level |
|
||||
ctx.message = `Access Level '${level.name}' created successfully.` |
|
||||
} |
|
||||
|
|
||||
exports.destroy = async function(ctx) { |
|
||||
const db = new CouchDB(ctx.user.appId) |
|
||||
await db.remove(ctx.params.levelId, ctx.params.rev) |
|
||||
ctx.message = `Access Level ${ctx.params.id} deleted successfully` |
|
||||
ctx.status = 200 |
|
||||
} |
|
||||
@ -0,0 +1,45 @@ |
|||||
|
const CouchDB = require("../../db") |
||||
|
const { |
||||
|
BUILTIN_ROLES, |
||||
|
Role, |
||||
|
getRole, |
||||
|
} = require("../../utilities/security/roles") |
||||
|
const { generateRoleID, getRoleParams } = require("../../db/utils") |
||||
|
|
||||
|
exports.fetch = async function(ctx) { |
||||
|
const db = new CouchDB(ctx.user.appId) |
||||
|
const body = await db.allDocs( |
||||
|
getRoleParams(null, { |
||||
|
include_docs: true, |
||||
|
}) |
||||
|
) |
||||
|
const customRoles = body.rows.map(row => row.doc) |
||||
|
|
||||
|
const staticRoles = [BUILTIN_ROLES.ADMIN, BUILTIN_ROLES.POWER] |
||||
|
ctx.body = [...staticRoles, ...customRoles] |
||||
|
} |
||||
|
|
||||
|
exports.find = async function(ctx) { |
||||
|
ctx.body = await getRole(ctx.user.appId, ctx.params.roleId) |
||||
|
} |
||||
|
|
||||
|
exports.save = async function(ctx) { |
||||
|
const db = new CouchDB(ctx.user.appId) |
||||
|
|
||||
|
let id = ctx.request.body._id || generateRoleID() |
||||
|
const role = new Role(id, ctx.request.body.name, ctx.request.body.inherits) |
||||
|
if (ctx.request.body._rev) { |
||||
|
role._rev = ctx.request.body._rev |
||||
|
} |
||||
|
const result = await db.put(role) |
||||
|
role._rev = result.rev |
||||
|
ctx.body = role |
||||
|
ctx.message = `Role '${role.name}' created successfully.` |
||||
|
} |
||||
|
|
||||
|
exports.destroy = async function(ctx) { |
||||
|
const db = new CouchDB(ctx.user.appId) |
||||
|
await db.remove(ctx.params.roleId, ctx.params.rev) |
||||
|
ctx.message = `Role ${ctx.params.id} deleted successfully` |
||||
|
ctx.status = 200 |
||||
|
} |
||||
@ -1,18 +0,0 @@ |
|||||
const Router = require("@koa/router") |
|
||||
const controller = require("../controllers/accesslevel") |
|
||||
const authorized = require("../../middleware/authorized") |
|
||||
const { BUILDER } = require("../../utilities/security/permissions") |
|
||||
|
|
||||
const router = Router() |
|
||||
|
|
||||
router |
|
||||
.post("/api/accesslevels", authorized(BUILDER), controller.save) |
|
||||
.get("/api/accesslevels", authorized(BUILDER), controller.fetch) |
|
||||
.get("/api/accesslevels/:levelId", authorized(BUILDER), controller.find) |
|
||||
.delete( |
|
||||
"/api/accesslevels/:levelId/:rev", |
|
||||
authorized(BUILDER), |
|
||||
controller.destroy |
|
||||
) |
|
||||
|
|
||||
module.exports = router |
|
||||
@ -0,0 +1,14 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const controller = require("../controllers/role") |
||||
|
const authorized = require("../../middleware/authorized") |
||||
|
const { BUILDER } = require("../../utilities/security/permissions") |
||||
|
|
||||
|
const router = Router() |
||||
|
|
||||
|
router |
||||
|
.post("/api/roles", authorized(BUILDER), controller.save) |
||||
|
.get("/api/roles", authorized(BUILDER), controller.fetch) |
||||
|
.get("/api/roles/:roleId", authorized(BUILDER), controller.find) |
||||
|
.delete("/api/roles/:roleId/:rev", authorized(BUILDER), controller.destroy) |
||||
|
|
||||
|
module.exports = router |
||||
@ -1,150 +0,0 @@ |
|||||
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 |
|
||||
@ -0,0 +1,143 @@ |
|||||
|
const CouchDB = require("../../db") |
||||
|
const { cloneDeep } = require("lodash/fp") |
||||
|
|
||||
|
const BUILTIN_IDS = { |
||||
|
ADMIN: "ADMIN", |
||||
|
POWER: "POWER_USER", |
||||
|
BASIC: "BASIC", |
||||
|
PUBLIC: "PUBLIC", |
||||
|
BUILDER: "BUILDER", |
||||
|
} |
||||
|
|
||||
|
function Role(id, name, inherits) { |
||||
|
this._id = id |
||||
|
this.name = name |
||||
|
if (inherits) { |
||||
|
this.inherits = inherits |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.BUILTIN_ROLES = { |
||||
|
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER), |
||||
|
POWER: new Role(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC), |
||||
|
BASIC: new Role(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.PUBLIC), |
||||
|
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public"), |
||||
|
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder"), |
||||
|
} |
||||
|
|
||||
|
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map( |
||||
|
level => level._id |
||||
|
) |
||||
|
|
||||
|
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map( |
||||
|
level => level.name |
||||
|
) |
||||
|
|
||||
|
function isBuiltin(role) { |
||||
|
return exports.BUILTIN_ROLE_ID_ARRAY.indexOf(role) !== -1 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
} |
||||
|
let role |
||||
|
if (isBuiltin(roleId)) { |
||||
|
role = cloneDeep( |
||||
|
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId) |
||||
|
) |
||||
|
} else { |
||||
|
const db = new CouchDB(appId) |
||||
|
role = await db.get(roleId) |
||||
|
} |
||||
|
return role |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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
|
||||
|
if (!userRoleId) { |
||||
|
return [BUILTIN_IDS.PUBLIC] |
||||
|
} |
||||
|
let roleIds = [userRoleId] |
||||
|
let userRole = await exports.getRole(appId, userRoleId) |
||||
|
// check if inherited makes it possible
|
||||
|
while ( |
||||
|
userRole && |
||||
|
userRole.inherits && |
||||
|
roleIds.indexOf(userRole.inherits) === -1 |
||||
|
) { |
||||
|
roleIds.push(userRole.inherits) |
||||
|
// go to get the inherited incase it inherits anything
|
||||
|
userRole = await exports.getRole(appId, userRole.inherits) |
||||
|
} |
||||
|
return roleIds |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
this.userHierarchies[userRoleId] = userRoleId |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.AccessController = AccessController |
||||
|
exports.BUILTIN_ROLE_IDS = BUILTIN_IDS |
||||
|
exports.isBuiltin = isBuiltin |
||||
|
exports.Role = Role |
||||
Loading…
Reference in new issue