forked from tsai/budibase
committed by
GitHub
23 changed files with 702 additions and 85 deletions
@ -1,6 +1,154 @@ |
|||
const { BUILTIN_PERMISSIONS } = require("../../utilities/security/permissions") |
|||
const { |
|||
BUILTIN_PERMISSIONS, |
|||
PermissionLevels, |
|||
higherPermission, |
|||
} = require("../../utilities/security/permissions") |
|||
const { |
|||
isBuiltin, |
|||
getDBRoleID, |
|||
getExternalRoleID, |
|||
BUILTIN_ROLES, |
|||
} = require("../../utilities/security/roles") |
|||
const { getRoleParams } = require("../../db/utils") |
|||
const CouchDB = require("../../db") |
|||
const { cloneDeep } = require("lodash/fp") |
|||
|
|||
exports.fetch = async function(ctx) { |
|||
// TODO: need to build out custom permissions
|
|||
const PermissionUpdateType = { |
|||
REMOVE: "remove", |
|||
ADD: "add", |
|||
} |
|||
|
|||
// utility function to stop this repetition - permissions always stored under roles
|
|||
async function getAllDBRoles(db) { |
|||
const body = await db.allDocs( |
|||
getRoleParams(null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
return body.rows.map(row => row.doc) |
|||
} |
|||
|
|||
async function updatePermissionOnRole( |
|||
appId, |
|||
{ roleId, resourceId, level }, |
|||
updateType |
|||
) { |
|||
const db = new CouchDB(appId) |
|||
const remove = updateType === PermissionUpdateType.REMOVE |
|||
const isABuiltin = isBuiltin(roleId) |
|||
const dbRoleId = getDBRoleID(roleId) |
|||
const dbRoles = await getAllDBRoles(db) |
|||
const docUpdates = [] |
|||
|
|||
// the permission is for a built in, make sure it exists
|
|||
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) { |
|||
const builtin = cloneDeep(BUILTIN_ROLES[roleId]) |
|||
builtin._id = getDBRoleID(builtin._id) |
|||
dbRoles.push(builtin) |
|||
} |
|||
|
|||
// now try to find any roles which need updated, e.g. removing the
|
|||
// resource from another role and then adding to the new role
|
|||
for (let role of dbRoles) { |
|||
let updated = false |
|||
const rolePermissions = role.permissions ? role.permissions : {} |
|||
// handle the removal/updating the role which has this permission first
|
|||
// the updating (role._id !== dbRoleId) is required because a resource/level can
|
|||
// only be permitted in a single role (this reduces hierarchy confusion and simplifies
|
|||
// the general UI for this, rather than needing to show everywhere it is used)
|
|||
if ( |
|||
(role._id !== dbRoleId || remove) && |
|||
rolePermissions[resourceId] === level |
|||
) { |
|||
delete rolePermissions[resourceId] |
|||
updated = true |
|||
} |
|||
// handle the adding, we're on the correct role, at it to this
|
|||
if (!remove && role._id === dbRoleId) { |
|||
rolePermissions[resourceId] = level |
|||
updated = true |
|||
} |
|||
// handle the update, add it to bulk docs to perform at end
|
|||
if (updated) { |
|||
role.permissions = rolePermissions |
|||
docUpdates.push(role) |
|||
} |
|||
} |
|||
|
|||
const response = await db.bulkDocs(docUpdates) |
|||
return response.map(resp => { |
|||
resp._id = getExternalRoleID(resp.id) |
|||
delete resp.id |
|||
return resp |
|||
}) |
|||
} |
|||
|
|||
exports.fetchBuiltin = function(ctx) { |
|||
ctx.body = Object.values(BUILTIN_PERMISSIONS) |
|||
} |
|||
|
|||
exports.fetchLevels = function(ctx) { |
|||
// for now only provide the read/write perms externally
|
|||
ctx.body = [PermissionLevels.WRITE, PermissionLevels.READ] |
|||
} |
|||
|
|||
exports.fetch = async function(ctx) { |
|||
const db = new CouchDB(ctx.appId) |
|||
const roles = await getAllDBRoles(db) |
|||
let permissions = {} |
|||
// create an object with structure role ID -> resource ID -> level
|
|||
for (let role of roles) { |
|||
if (role.permissions) { |
|||
const roleId = getExternalRoleID(role._id) |
|||
if (permissions[roleId] == null) { |
|||
permissions[roleId] = {} |
|||
} |
|||
for (let [resource, level] of Object.entries(role.permissions)) { |
|||
permissions[roleId][resource] = higherPermission( |
|||
permissions[roleId][resource], |
|||
level |
|||
) |
|||
} |
|||
} |
|||
} |
|||
ctx.body = permissions |
|||
} |
|||
|
|||
exports.getResourcePerms = async function(ctx) { |
|||
const resourceId = ctx.params.resourceId |
|||
const db = new CouchDB(ctx.appId) |
|||
const body = await db.allDocs( |
|||
getRoleParams(null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
const roles = body.rows.map(row => row.doc) |
|||
const resourcePerms = {} |
|||
for (let role of roles) { |
|||
// update the various roleIds in the resource permissions
|
|||
if (role.permissions && role.permissions[resourceId]) { |
|||
const roleId = getExternalRoleID(role._id) |
|||
resourcePerms[roleId] = higherPermission( |
|||
resourcePerms[roleId], |
|||
role.permissions[resourceId] |
|||
) |
|||
} |
|||
} |
|||
ctx.body = resourcePerms |
|||
} |
|||
|
|||
exports.addPermission = async function(ctx) { |
|||
ctx.body = await updatePermissionOnRole( |
|||
ctx.appId, |
|||
ctx.params, |
|||
PermissionUpdateType.ADD |
|||
) |
|||
} |
|||
|
|||
exports.removePermission = async function(ctx) { |
|||
ctx.body = await updatePermissionOnRole( |
|||
ctx.appId, |
|||
ctx.params, |
|||
PermissionUpdateType.REMOVE |
|||
) |
|||
} |
|||
|
|||
@ -1,10 +1,47 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../controllers/permission") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("../../utilities/security/permissions") |
|||
const { |
|||
BUILDER, |
|||
PermissionLevels, |
|||
} = require("../../utilities/security/permissions") |
|||
const Joi = require("joi") |
|||
const joiValidator = require("../../middleware/joi-validator") |
|||
|
|||
const router = Router() |
|||
|
|||
router.get("/api/permissions", authorized(BUILDER), controller.fetch) |
|||
function generateValidator() { |
|||
const permLevelArray = Object.values(PermissionLevels) |
|||
// prettier-ignore
|
|||
return joiValidator.params(Joi.object({ |
|||
level: Joi.string().valid(...permLevelArray).required(), |
|||
resourceId: Joi.string(), |
|||
roleId: Joi.string(), |
|||
}).unknown(true)) |
|||
} |
|||
|
|||
router |
|||
.get("/api/permission/builtin", authorized(BUILDER), controller.fetchBuiltin) |
|||
.get("/api/permission/levels", authorized(BUILDER), controller.fetchLevels) |
|||
.get("/api/permission", authorized(BUILDER), controller.fetch) |
|||
.get( |
|||
"/api/permission/:resourceId", |
|||
authorized(BUILDER), |
|||
controller.getResourcePerms |
|||
) |
|||
// adding a specific role/level for the resource overrides the underlying access control
|
|||
.post( |
|||
"/api/permission/:roleId/:resourceId/:level", |
|||
authorized(BUILDER), |
|||
generateValidator(), |
|||
controller.addPermission |
|||
) |
|||
// deleting the level defaults it back the underlying access control for the resource
|
|||
.delete( |
|||
"/api/permission/:roleId/:resourceId/:level", |
|||
authorized(BUILDER), |
|||
generateValidator(), |
|||
controller.removePermission |
|||
) |
|||
|
|||
module.exports = router |
|||
|
|||
@ -0,0 +1,125 @@ |
|||
const { |
|||
createApplication, |
|||
createTable, |
|||
createRow, |
|||
supertest, |
|||
defaultHeaders, |
|||
addPermission, |
|||
publicHeaders, |
|||
makeBasicRow, |
|||
} = require("./couchTestUtils") |
|||
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") |
|||
|
|||
const HIGHER_ROLE_ID = BUILTIN_ROLE_IDS.BASIC |
|||
const STD_ROLE_ID = BUILTIN_ROLE_IDS.PUBLIC |
|||
|
|||
describe("/permission", () => { |
|||
let server |
|||
let request |
|||
let appId |
|||
let table |
|||
let perms |
|||
let row |
|||
|
|||
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) |
|||
perms = await addPermission(request, appId, STD_ROLE_ID, table._id) |
|||
row = await createRow(request, appId, table._id) |
|||
}) |
|||
|
|||
async function getTablePermissions() { |
|||
return request |
|||
.get(`/api/permission/${table._id}`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
} |
|||
|
|||
describe("levels", () => { |
|||
it("should be able to get levels", async () => { |
|||
const res = await request |
|||
.get(`/api/permission/levels`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toBeDefined() |
|||
expect(res.body.length).toEqual(2) |
|||
expect(res.body).toContain("read") |
|||
expect(res.body).toContain("write") |
|||
}) |
|||
}) |
|||
|
|||
describe("add", () => { |
|||
it("should be able to add permission to a role for the table", async () => { |
|||
expect(perms.length).toEqual(1) |
|||
expect(perms[0]._id).toEqual(`${STD_ROLE_ID}`) |
|||
}) |
|||
|
|||
it("should get the resource permissions", async () => { |
|||
const res = await request |
|||
.get(`/api/permission/${table._id}`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body[STD_ROLE_ID]).toEqual("read") |
|||
}) |
|||
|
|||
it("should get resource permissions with multiple roles", async () => { |
|||
perms = await addPermission(request, appId, HIGHER_ROLE_ID, table._id, "write") |
|||
const res = await getTablePermissions() |
|||
expect(res.body[HIGHER_ROLE_ID]).toEqual("write") |
|||
expect(res.body[STD_ROLE_ID]).toEqual("read") |
|||
const allRes = await request |
|||
.get(`/api/permission`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(allRes.body[HIGHER_ROLE_ID][table._id]).toEqual("write") |
|||
expect(allRes.body[STD_ROLE_ID][table._id]).toEqual("read") |
|||
}) |
|||
}) |
|||
|
|||
describe("remove", () => { |
|||
it("should be able to remove the permission", async () => { |
|||
const res = await request |
|||
.delete(`/api/permission/${STD_ROLE_ID}/${table._id}/read`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body[0]._id).toEqual(STD_ROLE_ID) |
|||
const permsRes = await getTablePermissions() |
|||
expect(permsRes.body[STD_ROLE_ID]).toBeUndefined() |
|||
}) |
|||
}) |
|||
|
|||
describe("check public user allowed", () => { |
|||
it("should be able to read the row", async () => { |
|||
const res = await request |
|||
.get(`/api/${table._id}/rows`) |
|||
.set(publicHeaders(appId)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body[0]._id).toEqual(row._id) |
|||
}) |
|||
|
|||
it("shouldn't allow writing from a public user", async () => { |
|||
const res = await request |
|||
.post(`/api/${table._id}/rows`) |
|||
.send(makeBasicRow(table._id)) |
|||
.set(publicHeaders(appId)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(403) |
|||
expect(res.status).toEqual(403) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,59 @@ |
|||
class ResourceIdGetter { |
|||
constructor(ctxProperty) { |
|||
this.parameter = ctxProperty |
|||
this.main = null |
|||
this.sub = null |
|||
return this |
|||
} |
|||
|
|||
mainResource(field) { |
|||
this.main = field |
|||
return this |
|||
} |
|||
|
|||
subResource(field) { |
|||
this.sub = field |
|||
return this |
|||
} |
|||
|
|||
build() { |
|||
const parameter = this.parameter, |
|||
main = this.main, |
|||
sub = this.sub |
|||
return (ctx, next) => { |
|||
const request = ctx.request[parameter] || ctx[parameter] |
|||
if (request == null) { |
|||
return next() |
|||
} |
|||
if (main != null && request[main]) { |
|||
ctx.resourceId = request[main] |
|||
} |
|||
if (sub != null && request[sub]) { |
|||
ctx.subResourceId = request[sub] |
|||
} |
|||
return next() |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports.paramResource = main => { |
|||
return new ResourceIdGetter("params").mainResource(main).build() |
|||
} |
|||
|
|||
module.exports.paramSubResource = (main, sub) => { |
|||
return new ResourceIdGetter("params") |
|||
.mainResource(main) |
|||
.subResource(sub) |
|||
.build() |
|||
} |
|||
|
|||
module.exports.bodyResource = main => { |
|||
return new ResourceIdGetter("body").mainResource(main).build() |
|||
} |
|||
|
|||
module.exports.bodySubResource = (main, sub) => { |
|||
return new ResourceIdGetter("body") |
|||
.mainResource(main) |
|||
.subResource(sub) |
|||
.build() |
|||
} |
|||
Loading…
Reference in new issue