forked from tsai/budibase
35 changed files with 547 additions and 537 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,6 @@ |
|||||
|
const { BUILTIN_PERMISSIONS } = require("../../utilities/security/permissions") |
||||
|
|
||||
|
exports.fetch = async function(ctx) { |
||||
|
// TODO: need to build out custom permissions
|
||||
|
ctx.body = Object.values(BUILTIN_PERMISSIONS) |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
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) |
||||
|
|
||||
|
// exclude internal roles like builder
|
||||
|
const staticRoles = [ |
||||
|
BUILTIN_ROLES.ADMIN, |
||||
|
BUILTIN_ROLES.POWER, |
||||
|
BUILTIN_ROLES.BASIC, |
||||
|
BUILTIN_ROLES.PUBLIC, |
||||
|
] |
||||
|
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, name, inherits, permissionId } = ctx.request.body |
||||
|
if (!_id) { |
||||
|
_id = generateRoleID() |
||||
|
} |
||||
|
const role = new Role(_id, name) |
||||
|
.addPermission(permissionId) |
||||
|
.addInheritance(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,10 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const controller = require("../controllers/permission") |
||||
|
const authorized = require("../../middleware/authorized") |
||||
|
const { BUILDER } = require("../../utilities/security/permissions") |
||||
|
|
||||
|
const router = Router() |
||||
|
|
||||
|
router.get("/api/permissions", authorized(BUILDER), controller.fetch) |
||||
|
|
||||
|
module.exports = router |
||||
@ -0,0 +1,30 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const controller = require("../controllers/role") |
||||
|
const authorized = require("../../middleware/authorized") |
||||
|
const { BUILDER } = require("../../utilities/security/permissions") |
||||
|
const Joi = require("joi") |
||||
|
const joiValidator = require("../../middleware/joi-validator") |
||||
|
const { |
||||
|
BUILTIN_PERMISSION_IDS, |
||||
|
} = require("../../utilities/security/permissions") |
||||
|
|
||||
|
const router = Router() |
||||
|
|
||||
|
function generateValidator() { |
||||
|
// prettier-ignore
|
||||
|
return joiValidator.body(Joi.object({ |
||||
|
_id: Joi.string().optional(), |
||||
|
_rev: Joi.string().optional(), |
||||
|
name: Joi.string().required(), |
||||
|
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(), |
||||
|
inherits: Joi.string().optional(), |
||||
|
}).unknown(true)) |
||||
|
} |
||||
|
|
||||
|
router |
||||
|
.post("/api/roles", authorized(BUILDER), generateValidator(), 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,110 +0,0 @@ |
|||||
const { |
|
||||
createApplication, |
|
||||
createTable, |
|
||||
createView, |
|
||||
supertest, |
|
||||
defaultHeaders |
|
||||
} = require("./couchTestUtils") |
|
||||
const { |
|
||||
BUILTIN_LEVEL_IDS, |
|
||||
} = require("../../../utilities/security/accessLevels") |
|
||||
|
|
||||
const accessLevelBody = { name: "user", inherits: BUILTIN_LEVEL_IDS.BASIC } |
|
||||
|
|
||||
describe("/accesslevels", () => { |
|
||||
let server |
|
||||
let request |
|
||||
let appId |
|
||||
let table |
|
||||
let view |
|
||||
|
|
||||
beforeAll(async () => { |
|
||||
({ request, server } = await supertest()) |
|
||||
}); |
|
||||
|
|
||||
afterAll(() => { |
|
||||
server.close() |
|
||||
}) |
|
||||
|
|
||||
beforeEach(async () => { |
|
||||
let app = await createApplication(request) |
|
||||
appId = app.instance._id |
|
||||
table = await createTable(request, appId) |
|
||||
view = await createView(request, appId, table._id) |
|
||||
}) |
|
||||
|
|
||||
describe("create", () => { |
|
||||
|
|
||||
it("returns a success message when level is successfully created", async () => { |
|
||||
const res = await request |
|
||||
.post(`/api/accesslevels`) |
|
||||
.send(accessLevelBody) |
|
||||
.set(defaultHeaders(appId)) |
|
||||
.expect('Content-Type', /json/) |
|
||||
.expect(200) |
|
||||
|
|
||||
expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.") |
|
||||
expect(res.body._id).toBeDefined() |
|
||||
expect(res.body._rev).toBeDefined() |
|
||||
}) |
|
||||
|
|
||||
}); |
|
||||
|
|
||||
describe("fetch", () => { |
|
||||
|
|
||||
it("should list custom levels, plus 2 default levels", async () => { |
|
||||
const createRes = await request |
|
||||
.post(`/api/accesslevels`) |
|
||||
.send(accessLevelBody) |
|
||||
.set(defaultHeaders(appId)) |
|
||||
.expect('Content-Type', /json/) |
|
||||
.expect(200) |
|
||||
|
|
||||
const customLevel = createRes.body |
|
||||
|
|
||||
const res = await request |
|
||||
.get(`/api/accesslevels`) |
|
||||
.set(defaultHeaders(appId)) |
|
||||
.expect('Content-Type', /json/) |
|
||||
.expect(200) |
|
||||
|
|
||||
expect(res.body.length).toBe(3) |
|
||||
|
|
||||
const adminLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.ADMIN) |
|
||||
expect(adminLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.POWER) |
|
||||
expect(adminLevel).toBeDefined() |
|
||||
|
|
||||
const powerUserLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.POWER) |
|
||||
expect(powerUserLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC) |
|
||||
expect(powerUserLevel).toBeDefined() |
|
||||
|
|
||||
const customLevelFetched = res.body.find(r => r._id === customLevel._id) |
|
||||
expect(customLevelFetched.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC) |
|
||||
expect(customLevelFetched).toBeDefined() |
|
||||
}) |
|
||||
|
|
||||
}); |
|
||||
|
|
||||
describe("destroy", () => { |
|
||||
it("should delete custom access level", async () => { |
|
||||
const createRes = await request |
|
||||
.post(`/api/accesslevels`) |
|
||||
.send({ name: "user" }) |
|
||||
.set(defaultHeaders(appId)) |
|
||||
.expect('Content-Type', /json/) |
|
||||
.expect(200) |
|
||||
|
|
||||
const customLevel = createRes.body |
|
||||
|
|
||||
await request |
|
||||
.delete(`/api/accesslevels/${customLevel._id}/${customLevel._rev}`) |
|
||||
.set(defaultHeaders(appId)) |
|
||||
.expect(200) |
|
||||
|
|
||||
await request |
|
||||
.get(`/api/accesslevels/${customLevel._id}`) |
|
||||
.set(defaultHeaders(appId)) |
|
||||
.expect(404) |
|
||||
}) |
|
||||
}) |
|
||||
}); |
|
||||
@ -0,0 +1,114 @@ |
|||||
|
const { |
||||
|
createApplication, |
||||
|
createTable, |
||||
|
createView, |
||||
|
supertest, |
||||
|
defaultHeaders |
||||
|
} = require("./couchTestUtils") |
||||
|
const { |
||||
|
BUILTIN_ROLE_IDS, |
||||
|
} = require("../../../utilities/security/roles") |
||||
|
const { BUILTIN_PERMISSION_IDS } = require("../../../utilities/security/permissions") |
||||
|
|
||||
|
const roleBody = { name: "NewRole", inherits: BUILTIN_ROLE_IDS.BASIC, permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY } |
||||
|
|
||||
|
describe("/roles", () => { |
||||
|
let server |
||||
|
let request |
||||
|
let appId |
||||
|
let table |
||||
|
let view |
||||
|
|
||||
|
beforeAll(async () => { |
||||
|
({ request, server } = await supertest()) |
||||
|
}); |
||||
|
|
||||
|
afterAll(() => { |
||||
|
server.close() |
||||
|
}) |
||||
|
|
||||
|
beforeEach(async () => { |
||||
|
let app = await createApplication(request) |
||||
|
appId = app.instance._id |
||||
|
table = await createTable(request, appId) |
||||
|
view = await createView(request, appId, table._id) |
||||
|
}) |
||||
|
|
||||
|
describe("create", () => { |
||||
|
|
||||
|
it("returns a success message when role is successfully created", async () => { |
||||
|
const res = await request |
||||
|
.post(`/api/roles`) |
||||
|
.send(roleBody) |
||||
|
.set(defaultHeaders(appId)) |
||||
|
.expect('Content-Type', /json/) |
||||
|
.expect(200) |
||||
|
|
||||
|
expect(res.res.statusMessage).toEqual("Role 'NewRole' created successfully.") |
||||
|
expect(res.body._id).toBeDefined() |
||||
|
expect(res.body._rev).toBeDefined() |
||||
|
}) |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
describe("fetch", () => { |
||||
|
|
||||
|
it("should list custom roles, plus 2 default roles", async () => { |
||||
|
const createRes = await request |
||||
|
.post(`/api/roles`) |
||||
|
.send(roleBody) |
||||
|
.set(defaultHeaders(appId)) |
||||
|
.expect('Content-Type', /json/) |
||||
|
.expect(200) |
||||
|
|
||||
|
const customRole = createRes.body |
||||
|
|
||||
|
const res = await request |
||||
|
.get(`/api/roles`) |
||||
|
.set(defaultHeaders(appId)) |
||||
|
.expect('Content-Type', /json/) |
||||
|
.expect(200) |
||||
|
|
||||
|
expect(res.body.length).toBe(3) |
||||
|
|
||||
|
const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN) |
||||
|
expect(adminRole).toBeDefined() |
||||
|
expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER) |
||||
|
expect(adminRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.ADMIN) |
||||
|
|
||||
|
const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER) |
||||
|
expect(powerUserRole).toBeDefined() |
||||
|
expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) |
||||
|
expect(powerUserRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.POWER) |
||||
|
|
||||
|
const customRoleFetched = res.body.find(r => r._id === customRole._id) |
||||
|
expect(customRoleFetched).toBeDefined() |
||||
|
expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) |
||||
|
expect(customRoleFetched.permissionId).toEqual(BUILTIN_PERMISSION_IDS.READ_ONLY) |
||||
|
}) |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
describe("destroy", () => { |
||||
|
it("should delete custom roles", async () => { |
||||
|
const createRes = await request |
||||
|
.post(`/api/roles`) |
||||
|
.send({ name: "user", permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY }) |
||||
|
.set(defaultHeaders(appId)) |
||||
|
.expect('Content-Type', /json/) |
||||
|
.expect(200) |
||||
|
|
||||
|
const customRole = createRes.body |
||||
|
|
||||
|
await request |
||||
|
.delete(`/api/roles/${customRole._id}/${customRole._rev}`) |
||||
|
.set(defaultHeaders(appId)) |
||||
|
.expect(200) |
||||
|
|
||||
|
await request |
||||
|
.get(`/api/roles/${customRole._id}`) |
||||
|
.set(defaultHeaders(appId)) |
||||
|
.expect(404) |
||||
|
}) |
||||
|
}) |
||||
|
}); |
||||
@ -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,183 @@ |
|||||
|
const CouchDB = require("../../db") |
||||
|
const { cloneDeep } = require("lodash/fp") |
||||
|
const { BUILTIN_PERMISSION_IDS } = require("./permissions") |
||||
|
|
||||
|
const BUILTIN_IDS = { |
||||
|
ADMIN: "ADMIN", |
||||
|
POWER: "POWER_USER", |
||||
|
BASIC: "BASIC", |
||||
|
PUBLIC: "PUBLIC", |
||||
|
BUILDER: "BUILDER", |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
exports.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.READ_ONLY |
||||
|
), |
||||
|
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission( |
||||
|
BUILTIN_PERMISSION_IDS.ADMIN |
||||
|
), |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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<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
|
||||
|
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<string[]>} A list of permission IDs these should all be unique. |
||||
|
*/ |
||||
|
exports.getUserPermissionIds = async (appId, userRoleId) => { |
||||
|
return [ |
||||
|
...new Set( |
||||
|
(await getAllUserRoles(appId, userRoleId)).map(role => role.permissionId) |
||||
|
), |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
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