mirror of https://github.com/Budibase/budibase.git
34 changed files with 332 additions and 371 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,4 +1,3 @@ |
|||
class Integration { |
|||
constructor() { |
|||
} |
|||
} |
|||
constructor() {} |
|||
} |
|||
|
|||
@ -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