diff --git a/packages/server/src/api/controllers/permission.js b/packages/server/src/api/controllers/permission.js index c7ffcda672..cca1e0f696 100644 --- a/packages/server/src/api/controllers/permission.js +++ b/packages/server/src/api/controllers/permission.js @@ -1,17 +1,23 @@ const { BUILTIN_PERMISSIONS, PermissionLevels, + higherPermission, } = require("../../utilities/security/permissions") const { getRoleParams } = require("../../db/utils") const CouchDB = require("../../db") +const PermissionUpdateType = { + REMOVE: "remove", + ADD: "add", +} + async function updatePermissionOnRole( appId, - roleId, - permissions, - remove = false + { roleId, resourceId, level }, + updateType ) { const db = new CouchDB(appId) + const remove = updateType === PermissionUpdateType.REMOVE const body = await db.allDocs( getRoleParams(null, { include_docs: true, @@ -23,13 +29,32 @@ async function updatePermissionOnRole( // 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) { - if (role.permissions) { - // TODO + let updated = false + const rolePermissions = role.permissions ? role.permissions : {} + // handle the removal/updating the role which has this permission first + // the updating (role._id !== roleId) 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 !== roleId || 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 === roleId) { + 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) } } - // TODO: NEED TO WORK THIS PART OUT - return await db.bulkDocs(docUpdates) + return db.bulkDocs(docUpdates) } exports.fetchBuiltin = function(ctx) { @@ -37,19 +62,44 @@ exports.fetchBuiltin = function(ctx) { } exports.fetchLevels = function(ctx) { - ctx.body = Object.values(PermissionLevels) + // for now only provide the read/write perms externally + ctx.body = [PermissionLevels.WRITE, PermissionLevels.READ] +} + +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]) { + resourcePerms[role._id] = higherPermission( + resourcePerms[role._id], + role.permissions[resourceId] + ) + } + } + ctx.body = resourcePerms } exports.addPermission = async function(ctx) { - const appId = ctx.appId, - roleId = ctx.params.roleId, - resourceId = ctx.params.resourceId - ctx.body = await updatePermissionOnRole(appId, roleId, resourceId) + ctx.body = await updatePermissionOnRole( + ctx.appId, + ctx.params, + PermissionUpdateType.ADD + ) } exports.removePermission = async function(ctx) { - const appId = ctx.appId, - roleId = ctx.params.roleId, - resourceId = ctx.params.resourceId - ctx.body = await updatePermissionOnRole(appId, roleId, resourceId, true) + ctx.body = await updatePermissionOnRole( + ctx.appId, + ctx.params, + PermissionUpdateType.REMOVE + ) } diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 904dd08ed1..857d1dd2ad 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -54,7 +54,7 @@ async function findRow(db, appId, tableId, rowId) { exports.patch = async function(ctx) { const appId = ctx.user.appId const db = new CouchDB(appId) - let row = await db.get(ctx.params.id) + let row = await db.get(ctx.params.rowId) const table = await db.get(row.tableId) const patchfields = ctx.request.body @@ -123,7 +123,7 @@ exports.save = async function(ctx) { // if the row obj had an _id then it will have been retrieved const existingRow = ctx.preExisting if (existingRow) { - ctx.params.id = row._id + ctx.params.rowId = row._id await exports.patch(ctx) return } diff --git a/packages/server/src/api/routes/automation.js b/packages/server/src/api/routes/automation.js index 8644c75787..3088bc521f 100644 --- a/packages/server/src/api/routes/automation.js +++ b/packages/server/src/api/routes/automation.js @@ -8,6 +8,7 @@ const { PermissionTypes, } = require("../../utilities/security/permissions") const Joi = require("joi") +const { bodyResource, paramResource } = require("../../middleware/resourceId") const router = Router() @@ -64,9 +65,15 @@ router controller.getDefinitionList ) .get("/api/automations", authorized(BUILDER), controller.fetch) - .get("/api/automations/:id", authorized(BUILDER), controller.find) + .get( + "/api/automations/:id", + paramResource("id"), + authorized(BUILDER), + controller.find + ) .put( "/api/automations", + bodyResource("_id"), authorized(BUILDER), generateValidator(true), controller.update @@ -79,9 +86,15 @@ router ) .post( "/api/automations/:id/trigger", + paramResource("id"), authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), controller.trigger ) - .delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy) + .delete( + "/api/automations/:id/:rev", + paramResource("id"), + authorized(BUILDER), + controller.destroy + ) module.exports = router diff --git a/packages/server/src/api/routes/permission.js b/packages/server/src/api/routes/permission.js index aa312d6537..1d7c042f99 100644 --- a/packages/server/src/api/routes/permission.js +++ b/packages/server/src/api/routes/permission.js @@ -10,34 +10,36 @@ const joiValidator = require("../../middleware/joi-validator") const router = Router() -function generateAddValidator() { +function generateValidator() { const permLevelArray = Object.values(PermissionLevels) // prettier-ignore - return joiValidator.body(Joi.object({ - permissions: Joi.object() - .pattern(/.*/, [Joi.string().valid(...permLevelArray)]) - .required() - }).unknown(true)) -} - -function generateRemoveValidator() { - // prettier-ignore - return joiValidator.body(Joi.object({ - permissions: Joi.array().items(Joi.string()) + 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) - .post( - "/api/permission/:roleId/:resourceId", + .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", + "/api/permission/:roleId/:resourceId/:level", authorized(BUILDER), + generateValidator(), controller.removePermission ) diff --git a/packages/server/src/api/routes/query.js b/packages/server/src/api/routes/query.js index 8a84138af5..ad71a53452 100644 --- a/packages/server/src/api/routes/query.js +++ b/packages/server/src/api/routes/query.js @@ -8,6 +8,11 @@ const { PermissionTypes, } = require("../../utilities/security/permissions") const joiValidator = require("../../middleware/joi-validator") +const { + bodyResource, + bodySubResource, + paramResource, +} = require("../../middleware/resourceId") const router = Router() @@ -50,23 +55,27 @@ router .get("/api/queries", authorized(BUILDER), queryController.fetch) .post( "/api/queries", + bodySubResource("datasourceId", "_id"), authorized(BUILDER), generateQueryValidation(), queryController.save ) .post( "/api/queries/preview", + bodyResource("datasourceId"), authorized(BUILDER), generateQueryPreviewValidation(), queryController.preview ) .post( "/api/queries/:queryId", + paramResource("queryId"), authorized(PermissionTypes.QUERY, PermissionLevels.WRITE), queryController.execute ) .delete( "/api/queries/:queryId/:revId", + paramResource("queryId"), authorized(BUILDER), queryController.destroy ) diff --git a/packages/server/src/api/routes/row.js b/packages/server/src/api/routes/row.js index 63964e3066..494ea61608 100644 --- a/packages/server/src/api/routes/row.js +++ b/packages/server/src/api/routes/row.js @@ -2,6 +2,10 @@ const Router = require("@koa/router") const rowController = require("../controllers/row") const authorized = require("../../middleware/authorized") const usage = require("../../middleware/usageQuota") +const { + paramResource, + paramSubResource, +} = require("../../middleware/resourceId") const { PermissionLevels, PermissionTypes, @@ -12,37 +16,44 @@ const router = Router() router .get( "/api/:tableId/:rowId/enrich", + paramSubResource("tableId", "rowId"), authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.fetchEnrichedRow ) .get( "/api/:tableId/rows", + paramResource("tableId"), authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.fetchTableRows ) .get( "/api/:tableId/rows/:rowId", + paramSubResource("tableId", "rowId"), authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.find ) .post( "/api/:tableId/rows", + paramResource("tableId"), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), usage, rowController.save ) .patch( - "/api/:tableId/rows/:id", + "/api/:tableId/rows/:rowId", + paramSubResource("tableId", "rowId"), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), rowController.patch ) .post( "/api/:tableId/rows/validate", + paramResource("tableId"), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), rowController.validate ) .delete( "/api/:tableId/rows/:rowId/:revId", + paramSubResource("tableId", "rowId"), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), usage, rowController.destroy diff --git a/packages/server/src/api/routes/table.js b/packages/server/src/api/routes/table.js index ef0eb7caec..da5c753b83 100644 --- a/packages/server/src/api/routes/table.js +++ b/packages/server/src/api/routes/table.js @@ -1,6 +1,7 @@ const Router = require("@koa/router") const tableController = require("../controllers/table") const authorized = require("../../middleware/authorized") +const { paramResource, bodyResource } = require("../../middleware/resourceId") const { BUILDER, PermissionLevels, @@ -13,10 +14,17 @@ router .get("/api/tables", authorized(BUILDER), tableController.fetch) .get( "/api/tables/:id", + paramResource("id"), authorized(PermissionTypes.TABLE, PermissionLevels.READ), tableController.find ) - .post("/api/tables", authorized(BUILDER), tableController.save) + .post( + "/api/tables", + // allows control over updating a table + bodyResource("_id"), + authorized(BUILDER), + tableController.save + ) .post( "/api/tables/csv/validate", authorized(BUILDER), @@ -24,6 +32,7 @@ router ) .delete( "/api/tables/:tableId/:revId", + paramResource("tableId"), authorized(BUILDER), tableController.destroy ) diff --git a/packages/server/src/middleware/joi-validator.js b/packages/server/src/middleware/joi-validator.js index 7ded06fe81..1686b0e727 100644 --- a/packages/server/src/middleware/joi-validator.js +++ b/packages/server/src/middleware/joi-validator.js @@ -22,3 +22,7 @@ function validate(schema, property) { module.exports.body = schema => { return validate(schema, "body") } + +module.exports.params = schema => { + return validate(schema, "params") +} diff --git a/packages/server/src/middleware/resourceId.js b/packages/server/src/middleware/resourceId.js new file mode 100644 index 0000000000..e21c7d0c5b --- /dev/null +++ b/packages/server/src/middleware/resourceId.js @@ -0,0 +1,55 @@ +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) => { + if (main != null && ctx.request[parameter][main]) { + ctx.resourceId = ctx.request[parameter][main] + } + if (sub != null && ctx.request[parameter][sub]) { + ctx.subResourceId = ctx.request[parameter][sub] + } + 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() +} diff --git a/packages/server/src/utilities/security/permissions.js b/packages/server/src/utilities/security/permissions.js index 12010dcc40..8e3dc4e831 100644 --- a/packages/server/src/utilities/security/permissions.js +++ b/packages/server/src/utilities/security/permissions.js @@ -30,12 +30,11 @@ function Permission(type, level) { */ function getAllowedLevels(userPermLevel) { switch (userPermLevel) { - case PermissionLevels.READ: - return [PermissionLevels.READ] - case PermissionLevels.WRITE: - return [PermissionLevels.READ, PermissionLevels.WRITE] case PermissionLevels.EXECUTE: return [PermissionLevels.EXECUTE] + case PermissionLevels.READ: + return [PermissionLevels.EXECUTE, PermissionLevels.READ] + case PermissionLevels.WRITE: case PermissionLevels.ADMIN: return [ PermissionLevels.READ, @@ -116,6 +115,25 @@ exports.doesHavePermission = (permType, permLevel, permissionIds) => { return false } +exports.higherPermission = (perm1, perm2) => { + function toNum(perm) { + switch (perm) { + // not everything has execute privileges + case PermissionLevels.EXECUTE: + return 0 + case PermissionLevels.READ: + return 1 + case PermissionLevels.WRITE: + return 2 + case PermissionLevels.ADMIN: + return 3 + default: + return -1 + } + } + return toNum(perm1) > toNum(perm2) ? perm1 : perm2 +} + // utility as a lot of things need simply the builder permission exports.BUILDER = PermissionTypes.BUILDER exports.PermissionTypes = PermissionTypes