1
0
Fork 0
mirror of synced 2024-06-23 08:30:31 +12:00

Merge branch 'feature/security-update' of github.com:Budibase/budibase into feature/page-refactor

This commit is contained in:
mike12345567 2020-12-02 13:26:57 +00:00
commit c832ed36d4
31 changed files with 328 additions and 365 deletions

View file

@ -111,7 +111,7 @@ Cypress.Commands.add("addRow", values => {
}) })
}) })
Cypress.Commands.add("createUser", (username, password, accessLevel) => { Cypress.Commands.add("createUser", (username, password, role) => {
// Create User // Create User
cy.contains("Users").click() cy.contains("Users").click()
@ -126,7 +126,7 @@ Cypress.Commands.add("createUser", (username, password, accessLevel) => {
.type(username) .type(username)
cy.get("select") cy.get("select")
.first() .first()
.select(accessLevel) .select(role)
// Save // Save
cy.get(".buttons") cy.get(".buttons")

View file

@ -18,7 +18,7 @@ export class Screen extends BaseStructure {
}, },
routing: { routing: {
route: "", route: "",
accessLevelId: "", roleId: "",
}, },
name: "screen-id", name: "screen-id",
} }

View file

@ -56,9 +56,7 @@
password: string().required( password: string().required(
"Please enter a password for your first user." "Please enter a password for your first user."
), ),
accessLevelId: string().required( roleId: string().required("You need to select a role for your user."),
"You need to select an access level for your user."
),
}, },
] ]
@ -79,9 +77,7 @@
if (hasKey) { if (hasKey) {
validationSchemas.shift() validationSchemas.shift()
validationSchemas = validationSchemas
steps.shift() steps.shift()
steps = steps
} }
// Handles form navigation // Handles form navigation
@ -166,7 +162,7 @@
name: $createAppStore.values.username, name: $createAppStore.values.username,
username: $createAppStore.values.username, username: $createAppStore.values.username,
password: $createAppStore.values.password, password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId, roleId: $createAppStore.values.roleId,
} }
const userResp = await api.post(`/api/users`, user) const userResp = await api.post(`/api/users`, user)
const json = await userResp.json() const json = await userResp.json()

View file

@ -21,7 +21,7 @@
placeholder="Password" placeholder="Password"
type="password" type="password"
error={blurred.password && validationErrors.password} /> error={blurred.password && validationErrors.password} />
<Select label="Access Level" secondary name="accessLevelId"> <Select label="Role" secondary name="roleId">
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
</Select> </Select>

View file

@ -70,7 +70,7 @@
draftScreen.props._instanceName = name draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent draftScreen.props._component = baseComponent
// TODO: need to fix this up correctly // TODO: need to fix this up correctly
draftScreen.routing = { route, accessLevelId: "ADMIN" } draftScreen.routing = { route, roleId: "ADMIN" }
await store.actions.screens.create(draftScreen) await store.actions.screens.create(draftScreen)
if (createLink) { if (createLink) {

View file

@ -10,7 +10,7 @@ export const FrontendTypes = {
} }
// fields on the user table that cannot be edited // fields on the user table that cannot be edited
export const UNEDITABLE_USER_FIELDS = ["username", "password", "accessLevelId"] export const UNEDITABLE_USER_FIELDS = ["username", "password", "roleId"]
export const DEFAULT_LAYOUTS = { export const DEFAULT_LAYOUTS = {
main: { main: {

View file

@ -52,9 +52,9 @@
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Jest - Access Levels", "name": "Jest - Roles",
"program": "${workspaceFolder}/node_modules/.bin/jest", "program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["accesslevel.spec", "--runInBand"], "args": ["role.spec", "--runInBand"],
"console": "integratedTerminal", "console": "integratedTerminal",
"internalConsoleOptions": "neverOpen", "internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true, "disableOptimisticBPs": true,

View file

@ -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
}

View file

@ -20,9 +20,9 @@ const {
generateScreenID, generateScreenID,
} = require("../../db/utils") } = require("../../db/utils")
const { const {
BUILTIN_LEVEL_IDS, BUILTIN_ROLE_IDS,
AccessController, AccessController,
} = require("../../utilities/security/accessLevels") } = require("../../utilities/security/roles")
const { const {
downloadExtractComponentLibraries, downloadExtractComponentLibraries,
} = require("../../utilities/createAppPackage") } = require("../../utilities/createAppPackage")
@ -56,10 +56,10 @@ async function getScreens(db) {
).rows.map(row => row.doc) ).rows.map(row => row.doc)
} }
function getUserAccessLevelId(ctx) { function getUserRoleId(ctx) {
return !ctx.user.accessLevel || !ctx.user.accessLevel._id return !ctx.user.role || !ctx.user.role._id
? BUILTIN_LEVEL_IDS.PUBLIC ? BUILTIN_ROLE_IDS.PUBLIC
: ctx.user.accessLevel._id : ctx.user.role._id
} }
async function createInstance(template) { async function createInstance(template) {
@ -111,11 +111,11 @@ exports.fetch = async function(ctx) {
exports.fetchAppDefinition = async function(ctx) { exports.fetchAppDefinition = async function(ctx) {
const db = new CouchDB(ctx.params.appId) const db = new CouchDB(ctx.params.appId)
const layouts = await getLayouts(db) const layouts = await getLayouts(db)
const userAccessLevelId = getUserAccessLevelId(ctx) const userRoleId = getUserRoleId(ctx)
const accessController = new AccessController(ctx.params.appId) const accessController = new AccessController(ctx.params.appId)
const screens = accessController.checkScreensAccess( const screens = accessController.checkScreensAccess(
await getScreens(db), await getScreens(db),
userAccessLevelId userRoleId
) )
ctx.body = { ctx.body = {
layouts, layouts,

View file

@ -32,7 +32,7 @@ exports.authenticate = async ctx => {
if (await bcrypt.compare(password, dbUser.password)) { if (await bcrypt.compare(password, dbUser.password)) {
const payload = { const payload = {
userId: dbUser._id, userId: dbUser._id,
accessLevelId: dbUser.accessLevelId, roleId: dbUser.roleId,
version: app.version, version: app.version,
permissions: dbUser.permissions || [], permissions: dbUser.permissions || [],
} }

View file

@ -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
}

View file

@ -1,8 +1,8 @@
const { getRoutingInfo } = require("../../utilities/routing") const { getRoutingInfo } = require("../../utilities/routing")
const { const {
getUserAccessLevelHierarchy, getUserRoleHierarchy,
BUILTIN_LEVEL_IDS, BUILTIN_ROLE_IDS,
} = require("../../utilities/security/accessLevels") } = require("../../utilities/security/roles")
const URL_SEPARATOR = "/" const URL_SEPARATOR = "/"
@ -33,15 +33,15 @@ Routing.prototype.getScreensProp = function(fullpath) {
return this.json[topLevel].subpaths[fullpath].screens return this.json[topLevel].subpaths[fullpath].screens
} }
Routing.prototype.addScreenId = function(fullpath, accessLevel, screenId) { Routing.prototype.addScreenId = function(fullpath, roleId, screenId) {
this.getScreensProp(fullpath)[accessLevel] = screenId this.getScreensProp(fullpath)[roleId] = screenId
} }
/** /**
* Gets the full routing structure by querying the routing view and processing the result into the tree. * Gets the full routing structure by querying the routing view and processing the result into the tree.
* @param {string} appId The application to produce the routing structure for. * @param {string} appId The application to produce the routing structure for.
* @returns {Promise<object>} The routing structure, this is the full structure designed for use in the builder, * @returns {Promise<object>} The routing structure, this is the full structure designed for use in the builder,
* if the client routing is required then the updateRoutingStructureForUserLevel should be used. * if the client routing is required then the updateRoutingStructureForUserRole should be used.
*/ */
async function getRoutingStructure(appId) { async function getRoutingStructure(appId) {
const screenRoutes = await getRoutingInfo(appId) const screenRoutes = await getRoutingInfo(appId)
@ -49,8 +49,8 @@ async function getRoutingStructure(appId) {
for (let screenRoute of screenRoutes) { for (let screenRoute of screenRoutes) {
let fullpath = screenRoute.routing.route let fullpath = screenRoute.routing.route
const accessLevel = screenRoute.routing.accessLevelId const roleId = screenRoute.routing.roleId
routing.addScreenId(fullpath, accessLevel, screenRoute.id) routing.addScreenId(fullpath, roleId, screenRoute.id)
} }
return { routes: routing.json } return { routes: routing.json }
@ -62,29 +62,26 @@ exports.fetch = async ctx => {
exports.clientFetch = async ctx => { exports.clientFetch = async ctx => {
const routing = await getRoutingStructure(ctx.appId) const routing = await getRoutingStructure(ctx.appId)
let accessLevelId = ctx.user.accessLevel._id let roleId = ctx.user.role._id
// builder is a special case, always return the full routing structure // builder is a special case, always return the full routing structure
if (accessLevelId === BUILTIN_LEVEL_IDS.BUILDER) { if (roleId === BUILTIN_ROLE_IDS.BUILDER) {
accessLevelId = BUILTIN_LEVEL_IDS.ADMIN roleId = BUILTIN_ROLE_IDS.ADMIN
} }
const accessLevelIds = await getUserAccessLevelHierarchy( const roleIds = await getUserRoleHierarchy(ctx.appId, roleId)
ctx.appId,
accessLevelId
)
for (let topLevel of Object.values(routing.routes)) { for (let topLevel of Object.values(routing.routes)) {
for (let subpathKey of Object.keys(topLevel.subpaths)) { for (let subpathKey of Object.keys(topLevel.subpaths)) {
let found = false let found = false
const subpath = topLevel.subpaths[subpathKey] const subpath = topLevel.subpaths[subpathKey]
const accessLevelOptions = Object.keys(subpath.screens) const roleOptions = Object.keys(subpath.screens)
if (accessLevelOptions.length === 1 && !accessLevelOptions[0]) { if (roleOptions.length === 1 && !roleOptions[0]) {
subpath.screenId = subpath.screens[accessLevelOptions[0]] subpath.screenId = subpath.screens[roleOptions[0]]
subpath.accessLevelId = BUILTIN_LEVEL_IDS.BASIC subpath.roleId = BUILTIN_ROLE_IDS.BASIC
found = true found = true
} else { } else {
for (let levelId of accessLevelIds) { for (let roleId of roleIds) {
if (accessLevelOptions.indexOf(levelId) !== -1) { if (roleOptions.indexOf(roleId) !== -1) {
subpath.screenId = subpath.screens[levelId] subpath.screenId = subpath.screens[roleId]
subpath.accessLevelId = levelId subpath.roleId = roleId
found = true found = true
break break
} }

View file

@ -1,6 +1,6 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils") const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/accessLevels") const { AccessController } = require("../../utilities/security/roles")
exports.fetch = async ctx => { exports.fetch = async ctx => {
const appId = ctx.user.appId const appId = ctx.user.appId
@ -16,7 +16,7 @@ exports.fetch = async ctx => {
ctx.body = await new AccessController(appId).checkScreensAccess( ctx.body = await new AccessController(appId).checkScreensAccess(
screens, screens,
ctx.user.accessLevel._id ctx.user.role._id
) )
} }

View file

@ -1,9 +1,7 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt") const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils") const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
const { const { BUILTIN_ROLE_ID_ARRAY } = require("../../utilities/security/roles")
BUILTIN_LEVEL_ID_ARRAY,
} = require("../../utilities/security/accessLevels")
const { const {
BUILTIN_PERMISSION_NAMES, BUILTIN_PERMISSION_NAMES,
} = require("../../utilities/security/permissions") } = require("../../utilities/security/permissions")
@ -20,21 +18,15 @@ exports.fetch = async function(ctx) {
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const { const { username, password, name, roleId, permissions } = ctx.request.body
username,
password,
name,
accessLevelId,
permissions,
} = ctx.request.body
if (!username || !password) { if (!username || !password) {
ctx.throw(400, "Username and Password Required.") ctx.throw(400, "Username and Password Required.")
} }
const accessLevel = await checkAccessLevel(db, accessLevelId) const role = await checkRole(db, roleId)
if (!accessLevel) ctx.throw(400, "Invalid Access Level") if (!role) ctx.throw(400, "Invalid Role")
const user = { const user = {
_id: generateUserID(username), _id: generateUserID(username),
@ -42,7 +34,7 @@ exports.create = async function(ctx) {
password: await bcrypt.hash(password), password: await bcrypt.hash(password),
name: name || username, name: name || username,
type: "user", type: "user",
accessLevelId, roleId,
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER], permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
tableId: ViewNames.USERS, tableId: ViewNames.USERS,
} }
@ -97,14 +89,14 @@ exports.find = async function(ctx) {
} }
} }
const checkAccessLevel = async (db, accessLevelId) => { const checkRole = async (db, roleId) => {
if (!accessLevelId) return if (!roleId) return
if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) { if (BUILTIN_ROLE_ID_ARRAY.indexOf(roleId) !== -1) {
return { return {
_id: accessLevelId, _id: roleId,
name: accessLevelId, name: roleId,
permissions: [], permissions: [],
} }
} }
return await db.get(accessLevelId) return await db.get(roleId)
} }

View file

@ -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

View file

@ -10,7 +10,7 @@ const staticRoutes = require("./static")
const componentRoutes = require("./component") const componentRoutes = require("./component")
const automationRoutes = require("./automation") const automationRoutes = require("./automation")
const webhookRoutes = require("./webhook") const webhookRoutes = require("./webhook")
const accesslevelRoutes = require("./accesslevel") const roleRoutes = require("./role")
const deployRoutes = require("./deploy") const deployRoutes = require("./deploy")
const apiKeysRoutes = require("./apikeys") const apiKeysRoutes = require("./apikeys")
const templatesRoutes = require("./templates") const templatesRoutes = require("./templates")
@ -26,7 +26,7 @@ exports.mainRoutes = [
automationRoutes, automationRoutes,
viewRoutes, viewRoutes,
componentRoutes, componentRoutes,
accesslevelRoutes, roleRoutes,
apiKeysRoutes, apiKeysRoutes,
templatesRoutes, templatesRoutes,
analyticsRoutes, analyticsRoutes,

View file

@ -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

View file

@ -5,9 +5,10 @@ const controller = require("../controllers/routing")
const router = Router() const router = Router()
// gets the full structure, not just the correct screen ID for your access level
router router
// gets correct structure for user role
.get("/api/routing/client", controller.clientFetch) .get("/api/routing/client", controller.clientFetch)
// gets the full structure, not just the correct screen ID for user role
.get("/api/routing", authorized(BUILDER), controller.fetch) .get("/api/routing", authorized(BUILDER), controller.fetch)
module.exports = router module.exports = router

View file

@ -14,7 +14,7 @@ function generateSaveValidation() {
name: Joi.string().required(), name: Joi.string().required(),
routing: Joi.object({ routing: Joi.object({
route: Joi.string().required(), route: Joi.string().required(),
accessLevelId: Joi.string().required().allow(""), roleId: Joi.string().required().allow(""),
}).required().unknown(true), }).required().unknown(true),
props: Joi.object({ props: Joi.object({
_id: Joi.string().required(), _id: Joi.string().required(),

View file

@ -1,8 +1,6 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const supertest = require("supertest") const supertest = require("supertest")
const { const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
BUILTIN_LEVEL_IDS,
} = require("../../../utilities/security/accessLevels")
const { const {
BUILTIN_PERMISSION_NAMES, BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions") } = require("../../../utilities/security/permissions")
@ -26,7 +24,7 @@ exports.supertest = async () => {
exports.defaultHeaders = appId => { exports.defaultHeaders = appId => {
const builderUser = { const builderUser = {
userId: "BUILDER", userId: "BUILDER",
accessLevelId: BUILTIN_LEVEL_IDS.BUILDER, roleId: BUILTIN_ROLE_IDS.BUILDER,
} }
const builderToken = jwt.sign(builderUser, env.JWT_SECRET) const builderToken = jwt.sign(builderUser, env.JWT_SECRET)
@ -128,7 +126,7 @@ exports.createUser = async (
name: "Bill", name: "Bill",
username, username,
password, password,
accessLevelId: BUILTIN_LEVEL_IDS.POWER, roleId: BUILTIN_ROLE_IDS.POWER,
}) })
return res.body return res.body
} }
@ -184,13 +182,13 @@ const createUserWithPermissions = async (
name: username, name: username,
username, username,
password, password,
accessLevelId: BUILTIN_LEVEL_IDS.POWER, roleId: BUILTIN_ROLE_IDS.POWER,
permissions, permissions,
}) })
const anonUser = { const anonUser = {
userId: "ANON", userId: "ANON",
accessLevelId: BUILTIN_LEVEL_IDS.PUBLIC, roleId: BUILTIN_ROLE_IDS.PUBLIC,
appId: appId, appId: appId,
version: packageJson.version, version: packageJson.version,
} }

View file

@ -6,12 +6,12 @@ const {
defaultHeaders defaultHeaders
} = require("./couchTestUtils") } = require("./couchTestUtils")
const { const {
BUILTIN_LEVEL_IDS, BUILTIN_ROLE_IDS,
} = require("../../../utilities/security/accessLevels") } = require("../../../utilities/security/roles")
const accessLevelBody = { name: "user", inherits: BUILTIN_LEVEL_IDS.BASIC } const roleBody = { name: "user", inherits: BUILTIN_ROLE_IDS.BASIC }
describe("/accesslevels", () => { describe("/roles", () => {
let server let server
let request let request
let appId let appId
@ -35,15 +35,15 @@ describe("/accesslevels", () => {
describe("create", () => { describe("create", () => {
it("returns a success message when level is successfully created", async () => { it("returns a success message when role is successfully created", async () => {
const res = await request const res = await request
.post(`/api/accesslevels`) .post(`/api/roles`)
.send(accessLevelBody) .send(roleBody)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.") expect(res.res.statusMessage).toEqual("Role 'user' created successfully.")
expect(res.body._id).toBeDefined() expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined() expect(res.body._rev).toBeDefined()
}) })
@ -52,57 +52,57 @@ describe("/accesslevels", () => {
describe("fetch", () => { describe("fetch", () => {
it("should list custom levels, plus 2 default levels", async () => { it("should list custom roles, plus 2 default roles", async () => {
const createRes = await request const createRes = await request
.post(`/api/accesslevels`) .post(`/api/roles`)
.send(accessLevelBody) .send(roleBody)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const customLevel = createRes.body const customRole = createRes.body
const res = await request const res = await request
.get(`/api/accesslevels`) .get(`/api/roles`)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body.length).toBe(3) expect(res.body.length).toBe(3)
const adminLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.ADMIN) const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN)
expect(adminLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.POWER) expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER)
expect(adminLevel).toBeDefined() expect(adminRole).toBeDefined()
const powerUserLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.POWER) const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER)
expect(powerUserLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC) expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(powerUserLevel).toBeDefined() expect(powerUserRole).toBeDefined()
const customLevelFetched = res.body.find(r => r._id === customLevel._id) const customRoleFetched = res.body.find(r => r._id === customRole._id)
expect(customLevelFetched.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC) expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(customLevelFetched).toBeDefined() expect(customRoleFetched).toBeDefined()
}) })
}); });
describe("destroy", () => { describe("destroy", () => {
it("should delete custom access level", async () => { it("should delete custom roles", async () => {
const createRes = await request const createRes = await request
.post(`/api/accesslevels`) .post(`/api/roles`)
.send({ name: "user" }) .send({ name: "user" })
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const customLevel = createRes.body const customRole = createRes.body
await request await request
.delete(`/api/accesslevels/${customLevel._id}/${customLevel._rev}`) .delete(`/api/roles/${customRole._id}/${customRole._rev}`)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect(200) .expect(200)
await request await request
.get(`/api/accesslevels/${customLevel._id}`) .get(`/api/roles/${customRole._id}`)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect(404) .expect(404)
}) })

View file

@ -9,8 +9,8 @@ const {
BUILTIN_PERMISSION_NAMES, BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions") } = require("../../../utilities/security/permissions")
const { const {
BUILTIN_LEVEL_IDS, BUILTIN_ROLE_IDS,
} = require("../../../utilities/security/accessLevels") } = require("../../../utilities/security/roles")
describe("/users", () => { describe("/users", () => {
let request let request
@ -67,7 +67,7 @@ describe("/users", () => {
const res = await request const res = await request
.post(`/api/users`) .post(`/api/users`)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: BUILTIN_LEVEL_IDS.POWER }) .send({ name: "Bill", username: "bill", password: "bills_password", roleId: BUILTIN_ROLE_IDS.POWER })
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -79,7 +79,7 @@ describe("/users", () => {
await testPermissionsForEndpoint({ await testPermissionsForEndpoint({
request, request,
method: "POST", method: "POST",
body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: BUILTIN_LEVEL_IDS.POWER }, body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", roleId: BUILTIN_ROLE_IDS.POWER },
url: `/api/users`, url: `/api/users`,
appId: appId, appId: appId,
permName1: BUILTIN_PERMISSION_NAMES.ADMIN, permName1: BUILTIN_PERMISSION_NAMES.ADMIN,

View file

@ -1,4 +1,4 @@
const accessLevels = require("../../utilities/security/accessLevels") const roles = require("../../utilities/security/roles")
const userController = require("../../api/controllers/user") const userController = require("../../api/controllers/user")
const env = require("../../environment") const env = require("../../environment")
const usage = require("../../utilities/usageQuota") const usage = require("../../utilities/usageQuota")
@ -11,7 +11,7 @@ module.exports.definition = {
type: "ACTION", type: "ACTION",
stepId: "CREATE_USER", stepId: "CREATE_USER",
inputs: { inputs: {
accessLevelId: accessLevels.BUILTIN_LEVEL_IDS.POWER, roleId: roles.BUILTIN_ROLE_IDS.POWER,
}, },
schema: { schema: {
inputs: { inputs: {
@ -25,14 +25,14 @@ module.exports.definition = {
customType: "password", customType: "password",
title: "Password", title: "Password",
}, },
accessLevelId: { roleId: {
type: "string", type: "string",
title: "Access Level", title: "Role",
enum: accessLevels.BUILTIN_LEVEL_ID_ARRAY, enum: roles.BUILTIN_ROLE_ID_ARRAY,
pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY, pretty: roles.BUILTIN_ROLE_NAME_ARRAY,
}, },
}, },
required: ["username", "password", "accessLevelId"], required: ["username", "password", "roleId"],
}, },
outputs: { outputs: {
properties: { properties: {
@ -59,13 +59,13 @@ module.exports.definition = {
} }
module.exports.run = async function({ inputs, appId, apiKey }) { module.exports.run = async function({ inputs, appId, apiKey }) {
const { username, password, accessLevelId } = inputs const { username, password, roleId } = inputs
const ctx = { const ctx = {
user: { user: {
appId: appId, appId: appId,
}, },
request: { request: {
body: { username, password, accessLevelId }, body: { username, password, roleId },
}, },
} }

View file

@ -1,4 +1,4 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels") const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
const AuthTypes = { const AuthTypes = {
APP: "app", APP: "app",
@ -24,14 +24,14 @@ const USERS_TABLE_SCHEMA = {
fieldName: "username", fieldName: "username",
name: "username", name: "username",
}, },
accessLevelId: { roleId: {
fieldName: "accessLevelId", fieldName: "roleId",
name: "accessLevelId", name: "roleId",
type: "options", type: "options",
constraints: { constraints: {
type: "string", type: "string",
presence: false, presence: false,
inclusion: Object.keys(BUILTIN_LEVEL_IDS), inclusion: Object.keys(BUILTIN_ROLE_IDS),
}, },
}, },
}, },

View file

@ -1,4 +1,4 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels") const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
exports.HOME_SCREEN = { exports.HOME_SCREEN = {
description: "", description: "",
@ -97,7 +97,7 @@ exports.HOME_SCREEN = {
}, },
routing: { routing: {
route: "/", route: "/",
accessLevelId: BUILTIN_LEVEL_IDS.BASIC, roleId: BUILTIN_ROLE_IDS.BASIC,
}, },
name: "home-screen", name: "home-screen",
} }

View file

@ -10,7 +10,7 @@ const DocumentTypes = {
AUTOMATION: "au", AUTOMATION: "au",
LINK: "li", LINK: "li",
APP: "app", APP: "app",
ACCESS_LEVEL: "ac", ROLE: "role",
WEBHOOK: "wh", WEBHOOK: "wh",
INSTANCE: "inst", INSTANCE: "inst",
LAYOUT: "layout", LAYOUT: "layout",
@ -169,18 +169,18 @@ exports.getAppParams = (appId = null, otherProps = {}) => {
} }
/** /**
* Generates a new access level ID. * Generates a new role ID.
* @returns {string} The new access level ID which the access level doc can be stored under. * @returns {string} The new role ID which the role doc can be stored under.
*/ */
exports.generateAccessLevelID = () => { exports.generateRoleID = () => {
return `${DocumentTypes.ACCESS_LEVEL}${SEPARATOR}${newid()}` return `${DocumentTypes.ROLE}${SEPARATOR}${newid()}`
} }
/** /**
* Gets parameters for retrieving an access level, this is a utility function for the getDocParams function. * Gets parameters for retrieving a role, this is a utility function for the getDocParams function.
*/ */
exports.getAccessLevelParams = (accessLevelId = null, otherProps = {}) => { exports.getRoleParams = (roleId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.ACCESS_LEVEL, accessLevelId, otherProps) return getDocParams(DocumentTypes.ROLE, roleId, otherProps)
} }
/** /**

View file

@ -1,9 +1,6 @@
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes") const STATUS_CODES = require("../utilities/statusCodes")
const { const { getRole, BUILTIN_ROLES } = require("../utilities/security/roles")
getAccessLevel,
BUILTIN_LEVELS,
} = require("../utilities/security/accessLevels")
const { AuthTypes } = require("../constants") const { AuthTypes } = require("../constants")
const { getAppId, getCookieName, setCookie, isClient } = require("../utilities") const { getAppId, getCookieName, setCookie, isClient } = require("../utilities")
@ -35,7 +32,7 @@ module.exports = async (ctx, next) => {
ctx.appId = appId ctx.appId = appId
ctx.user = { ctx.user = {
appId, appId,
accessLevel: BUILTIN_LEVELS.PUBLIC, role: BUILTIN_ROLES.PUBLIC,
} }
await next() await next()
return return
@ -49,7 +46,7 @@ module.exports = async (ctx, next) => {
ctx.user = { ctx.user = {
...jwtPayload, ...jwtPayload,
appId: appId, appId: appId,
accessLevel: await getAccessLevel(appId, jwtPayload.accessLevelId), role: await getRole(appId, jwtPayload.roleId),
} }
} catch (err) { } catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)

View file

@ -1,4 +1,4 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels") const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
const { const {
PermissionTypes, PermissionTypes,
doesHavePermission, doesHavePermission,
@ -7,7 +7,7 @@ const env = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient") const { apiKeyTable } = require("../db/dynamoClient")
const { AuthTypes } = require("../constants") const { AuthTypes } = require("../constants")
const ADMIN_ACCESS = [BUILTIN_LEVEL_IDS.ADMIN, BUILTIN_LEVEL_IDS.BUILDER] const ADMIN_ROLES = [BUILTIN_ROLE_IDS.ADMIN, BUILTIN_ROLE_IDS.BUILDER]
const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|")) const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|"))
@ -47,9 +47,9 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
ctx.throw(403, "User not found") ctx.throw(403, "User not found")
} }
const accessLevel = ctx.user.accessLevel const role = ctx.user.role
const permissions = ctx.user.permissions const permissions = ctx.user.permissions
if (ADMIN_ACCESS.indexOf(accessLevel._id) !== -1) { if (ADMIN_ROLES.indexOf(role._id) !== -1) {
return next() return next()
} }

View file

@ -1,4 +1,4 @@
const { BUILTIN_LEVEL_IDS } = require("../security/accessLevels") const { BUILTIN_ROLE_IDS } = require("../security/roles")
const { BUILTIN_PERMISSION_NAMES } = require("../security/permissions") const { BUILTIN_PERMISSION_NAMES } = require("../security/permissions")
const env = require("../../environment") const env = require("../../environment")
const CouchDB = require("../../db") const CouchDB = require("../../db")
@ -10,7 +10,7 @@ const APP_PREFIX = DocumentTypes.APP + SEPARATOR
module.exports = async (ctx, appId, version) => { module.exports = async (ctx, appId, version) => {
const builderUser = { const builderUser = {
userId: "BUILDER", userId: "BUILDER",
accessLevelId: BUILTIN_LEVEL_IDS.BUILDER, roleId: BUILTIN_ROLE_IDS.BUILDER,
permissions: [BUILTIN_PERMISSION_NAMES.ADMIN], permissions: [BUILTIN_PERMISSION_NAMES.ADMIN],
version, version,
} }

View file

@ -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

View file

@ -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