Adding in resource IDs everywhere they should be accessible.

This commit is contained in:
mike12345567 2021-02-08 17:22:07 +00:00
parent 36edf3788f
commit cd729192ea
10 changed files with 212 additions and 41 deletions

View File

@ -1,17 +1,23 @@
const { const {
BUILTIN_PERMISSIONS, BUILTIN_PERMISSIONS,
PermissionLevels, PermissionLevels,
higherPermission,
} = require("../../utilities/security/permissions") } = require("../../utilities/security/permissions")
const { getRoleParams } = require("../../db/utils") const { getRoleParams } = require("../../db/utils")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const PermissionUpdateType = {
REMOVE: "remove",
ADD: "add",
}
async function updatePermissionOnRole( async function updatePermissionOnRole(
appId, appId,
roleId, { roleId, resourceId, level },
permissions, updateType
remove = false
) { ) {
const db = new CouchDB(appId) const db = new CouchDB(appId)
const remove = updateType === PermissionUpdateType.REMOVE
const body = await db.allDocs( const body = await db.allDocs(
getRoleParams(null, { getRoleParams(null, {
include_docs: true, include_docs: true,
@ -23,13 +29,32 @@ async function updatePermissionOnRole(
// now try to find any roles which need updated, e.g. removing the // now try to find any roles which need updated, e.g. removing the
// resource from another role and then adding to the new role // resource from another role and then adding to the new role
for (let role of dbRoles) { for (let role of dbRoles) {
if (role.permissions) { let updated = false
// TODO 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 db.bulkDocs(docUpdates)
return await db.bulkDocs(docUpdates)
} }
exports.fetchBuiltin = function(ctx) { exports.fetchBuiltin = function(ctx) {
@ -37,19 +62,44 @@ exports.fetchBuiltin = function(ctx) {
} }
exports.fetchLevels = 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) { exports.addPermission = async function(ctx) {
const appId = ctx.appId, ctx.body = await updatePermissionOnRole(
roleId = ctx.params.roleId, ctx.appId,
resourceId = ctx.params.resourceId ctx.params,
ctx.body = await updatePermissionOnRole(appId, roleId, resourceId) PermissionUpdateType.ADD
)
} }
exports.removePermission = async function(ctx) { exports.removePermission = async function(ctx) {
const appId = ctx.appId, ctx.body = await updatePermissionOnRole(
roleId = ctx.params.roleId, ctx.appId,
resourceId = ctx.params.resourceId ctx.params,
ctx.body = await updatePermissionOnRole(appId, roleId, resourceId, true) PermissionUpdateType.REMOVE
)
} }

View File

@ -54,7 +54,7 @@ async function findRow(db, appId, tableId, rowId) {
exports.patch = async function(ctx) { exports.patch = async function(ctx) {
const appId = ctx.user.appId const appId = ctx.user.appId
const db = new CouchDB(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 table = await db.get(row.tableId)
const patchfields = ctx.request.body 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 // if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting const existingRow = ctx.preExisting
if (existingRow) { if (existingRow) {
ctx.params.id = row._id ctx.params.rowId = row._id
await exports.patch(ctx) await exports.patch(ctx)
return return
} }

View File

@ -8,6 +8,7 @@ const {
PermissionTypes, PermissionTypes,
} = require("../../utilities/security/permissions") } = require("../../utilities/security/permissions")
const Joi = require("joi") const Joi = require("joi")
const { bodyResource, paramResource } = require("../../middleware/resourceId")
const router = Router() const router = Router()
@ -64,9 +65,15 @@ router
controller.getDefinitionList controller.getDefinitionList
) )
.get("/api/automations", authorized(BUILDER), controller.fetch) .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( .put(
"/api/automations", "/api/automations",
bodyResource("_id"),
authorized(BUILDER), authorized(BUILDER),
generateValidator(true), generateValidator(true),
controller.update controller.update
@ -79,9 +86,15 @@ router
) )
.post( .post(
"/api/automations/:id/trigger", "/api/automations/:id/trigger",
paramResource("id"),
authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
controller.trigger 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 module.exports = router

View File

@ -10,34 +10,36 @@ const joiValidator = require("../../middleware/joi-validator")
const router = Router() const router = Router()
function generateAddValidator() { function generateValidator() {
const permLevelArray = Object.values(PermissionLevels) const permLevelArray = Object.values(PermissionLevels)
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.params(Joi.object({
permissions: Joi.object() level: Joi.string().valid(...permLevelArray).required(),
.pattern(/.*/, [Joi.string().valid(...permLevelArray)]) resourceId: Joi.string(),
.required() roleId: Joi.string(),
}).unknown(true))
}
function generateRemoveValidator() {
// prettier-ignore
return joiValidator.body(Joi.object({
permissions: Joi.array().items(Joi.string())
}).unknown(true)) }).unknown(true))
} }
router router
.get("/api/permission/builtin", authorized(BUILDER), controller.fetchBuiltin) .get("/api/permission/builtin", authorized(BUILDER), controller.fetchBuiltin)
.get("/api/permission/levels", authorized(BUILDER), controller.fetchLevels) .get("/api/permission/levels", authorized(BUILDER), controller.fetchLevels)
.post( .get(
"/api/permission/:roleId/:resourceId", "/api/permission/:resourceId",
authorized(BUILDER), 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 controller.addPermission
) )
// deleting the level defaults it back the underlying access control for the resource
.delete( .delete(
"/api/permission/:roleId/:resourceId", "/api/permission/:roleId/:resourceId/:level",
authorized(BUILDER), authorized(BUILDER),
generateValidator(),
controller.removePermission controller.removePermission
) )

View File

@ -8,6 +8,11 @@ const {
PermissionTypes, PermissionTypes,
} = require("../../utilities/security/permissions") } = require("../../utilities/security/permissions")
const joiValidator = require("../../middleware/joi-validator") const joiValidator = require("../../middleware/joi-validator")
const {
bodyResource,
bodySubResource,
paramResource,
} = require("../../middleware/resourceId")
const router = Router() const router = Router()
@ -50,23 +55,27 @@ router
.get("/api/queries", authorized(BUILDER), queryController.fetch) .get("/api/queries", authorized(BUILDER), queryController.fetch)
.post( .post(
"/api/queries", "/api/queries",
bodySubResource("datasourceId", "_id"),
authorized(BUILDER), authorized(BUILDER),
generateQueryValidation(), generateQueryValidation(),
queryController.save queryController.save
) )
.post( .post(
"/api/queries/preview", "/api/queries/preview",
bodyResource("datasourceId"),
authorized(BUILDER), authorized(BUILDER),
generateQueryPreviewValidation(), generateQueryPreviewValidation(),
queryController.preview queryController.preview
) )
.post( .post(
"/api/queries/:queryId", "/api/queries/:queryId",
paramResource("queryId"),
authorized(PermissionTypes.QUERY, PermissionLevels.WRITE), authorized(PermissionTypes.QUERY, PermissionLevels.WRITE),
queryController.execute queryController.execute
) )
.delete( .delete(
"/api/queries/:queryId/:revId", "/api/queries/:queryId/:revId",
paramResource("queryId"),
authorized(BUILDER), authorized(BUILDER),
queryController.destroy queryController.destroy
) )

View File

@ -2,6 +2,10 @@ const Router = require("@koa/router")
const rowController = require("../controllers/row") const rowController = require("../controllers/row")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const usage = require("../../middleware/usageQuota") const usage = require("../../middleware/usageQuota")
const {
paramResource,
paramSubResource,
} = require("../../middleware/resourceId")
const { const {
PermissionLevels, PermissionLevels,
PermissionTypes, PermissionTypes,
@ -12,37 +16,44 @@ const router = Router()
router router
.get( .get(
"/api/:tableId/:rowId/enrich", "/api/:tableId/:rowId/enrich",
paramSubResource("tableId", "rowId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.fetchEnrichedRow rowController.fetchEnrichedRow
) )
.get( .get(
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.fetchTableRows rowController.fetchTableRows
) )
.get( .get(
"/api/:tableId/rows/:rowId", "/api/:tableId/rows/:rowId",
paramSubResource("tableId", "rowId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.find rowController.find
) )
.post( .post(
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage, usage,
rowController.save rowController.save
) )
.patch( .patch(
"/api/:tableId/rows/:id", "/api/:tableId/rows/:rowId",
paramSubResource("tableId", "rowId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
rowController.patch rowController.patch
) )
.post( .post(
"/api/:tableId/rows/validate", "/api/:tableId/rows/validate",
paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
rowController.validate rowController.validate
) )
.delete( .delete(
"/api/:tableId/rows/:rowId/:revId", "/api/:tableId/rows/:rowId/:revId",
paramSubResource("tableId", "rowId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage, usage,
rowController.destroy rowController.destroy

View File

@ -1,6 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const tableController = require("../controllers/table") const tableController = require("../controllers/table")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { paramResource, bodyResource } = require("../../middleware/resourceId")
const { const {
BUILDER, BUILDER,
PermissionLevels, PermissionLevels,
@ -13,10 +14,17 @@ router
.get("/api/tables", authorized(BUILDER), tableController.fetch) .get("/api/tables", authorized(BUILDER), tableController.fetch)
.get( .get(
"/api/tables/:id", "/api/tables/:id",
paramResource("id"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
tableController.find 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( .post(
"/api/tables/csv/validate", "/api/tables/csv/validate",
authorized(BUILDER), authorized(BUILDER),
@ -24,6 +32,7 @@ router
) )
.delete( .delete(
"/api/tables/:tableId/:revId", "/api/tables/:tableId/:revId",
paramResource("tableId"),
authorized(BUILDER), authorized(BUILDER),
tableController.destroy tableController.destroy
) )

View File

@ -22,3 +22,7 @@ function validate(schema, property) {
module.exports.body = schema => { module.exports.body = schema => {
return validate(schema, "body") return validate(schema, "body")
} }
module.exports.params = schema => {
return validate(schema, "params")
}

View File

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

View File

@ -30,12 +30,11 @@ function Permission(type, level) {
*/ */
function getAllowedLevels(userPermLevel) { function getAllowedLevels(userPermLevel) {
switch (userPermLevel) { switch (userPermLevel) {
case PermissionLevels.READ:
return [PermissionLevels.READ]
case PermissionLevels.WRITE:
return [PermissionLevels.READ, PermissionLevels.WRITE]
case PermissionLevels.EXECUTE: case PermissionLevels.EXECUTE:
return [PermissionLevels.EXECUTE] return [PermissionLevels.EXECUTE]
case PermissionLevels.READ:
return [PermissionLevels.EXECUTE, PermissionLevels.READ]
case PermissionLevels.WRITE:
case PermissionLevels.ADMIN: case PermissionLevels.ADMIN:
return [ return [
PermissionLevels.READ, PermissionLevels.READ,
@ -116,6 +115,25 @@ exports.doesHavePermission = (permType, permLevel, permissionIds) => {
return false 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 // utility as a lot of things need simply the builder permission
exports.BUILDER = PermissionTypes.BUILDER exports.BUILDER = PermissionTypes.BUILDER
exports.PermissionTypes = PermissionTypes exports.PermissionTypes = PermissionTypes