Merge branch 'master' of https://github.com/Budibase/budibase into internal-search
This commit is contained in:
commit
1a945da513
|
@ -84,7 +84,7 @@ services:
|
||||||
#- "4369:4369"
|
#- "4369:4369"
|
||||||
#- "9100:9100"
|
#- "9100:9100"
|
||||||
volumes:
|
volumes:
|
||||||
- couchdb_data:/couchdb
|
- couchdb_data:/opt/couchdb/data
|
||||||
|
|
||||||
couch-init:
|
couch-init:
|
||||||
image: curlimages/curl
|
image: curlimages/curl
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.58.2",
|
"@budibase/bbui": "^1.58.3",
|
||||||
"@budibase/client": "^0.7.6",
|
"@budibase/client": "^0.7.6",
|
||||||
"@budibase/colorpicker": "1.0.1",
|
"@budibase/colorpicker": "1.0.1",
|
||||||
"@budibase/string-templates": "^0.7.6",
|
"@budibase/string-templates": "^0.7.6",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
|
||||||
let permissions = []
|
let basePermissions = []
|
||||||
let selectedRole = {}
|
let selectedRole = {}
|
||||||
let errors = []
|
let errors = []
|
||||||
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
||||||
|
@ -16,9 +16,9 @@
|
||||||
)
|
)
|
||||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||||
|
|
||||||
const fetchPermissions = async () => {
|
const fetchBasePermissions = async () => {
|
||||||
const permissionsResponse = await api.get("/api/permissions")
|
const permissionsResponse = await api.get("/api/permission/builtin")
|
||||||
permissions = await permissionsResponse.json()
|
basePermissions = await permissionsResponse.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changes the selected role
|
// Changes the selected role
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(fetchPermissions)
|
onMount(fetchBasePermissions)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -121,11 +121,11 @@
|
||||||
<Select
|
<Select
|
||||||
thin
|
thin
|
||||||
secondary
|
secondary
|
||||||
label="Permissions"
|
label="Base Permissions"
|
||||||
bind:value={selectedRole.permissionId}>
|
bind:value={selectedRole.permissionId}>
|
||||||
<option value="">Choose permissions</option>
|
<option value="">Choose permissions</option>
|
||||||
{#each permissions as permission}
|
{#each basePermissions as basePerm}
|
||||||
<option value={permission._id}>{permission.name}</option>
|
<option value={basePerm._id}>{basePerm.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -842,10 +842,10 @@
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/bbui@^1.58.2":
|
"@budibase/bbui@^1.58.3":
|
||||||
version "1.58.2"
|
version "1.58.3"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.2.tgz#1b9a5b1bf20597c1ea85ebba69acfec01ef6edce"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.3.tgz#86ad6aa68eec7426e1ccdf1f7e7fc957cb57d3a3"
|
||||||
integrity sha512-Gn4yCNpoVhtVhRDuWEYdaBK/oAfccTvehywgbyH4sHKaY7aQ7v0679nsJsOHBjNPleKy5YN3ZLhneh5k3F1O2Q==
|
integrity sha512-PpbxfBhVpmP0EO1nPBhrz486EHCIgtJlXELC/ElzjG+FCQZSCvDSM7mq/97FOW35iYdTiQTlwFgOtvOgT1P8IQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it "^12.0.2"
|
markdown-it "^12.0.2"
|
||||||
quill "^1.3.7"
|
quill "^1.3.7"
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports.fetchInfo = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.save = async ctx => {
|
exports.save = async ctx => {
|
||||||
|
console.trace("DID A SAVE!")
|
||||||
const db = new CouchDB(BUILDER_CONFIG_DB)
|
const db = new CouchDB(BUILDER_CONFIG_DB)
|
||||||
const { type } = ctx.request.body
|
const { type } = ctx.request.body
|
||||||
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {
|
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {
|
||||||
|
|
|
@ -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) {
|
const PermissionUpdateType = {
|
||||||
// TODO: need to build out custom permissions
|
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)
|
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,8 +1,11 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const {
|
const {
|
||||||
BUILTIN_ROLES,
|
BUILTIN_ROLES,
|
||||||
|
BUILTIN_ROLE_IDS,
|
||||||
Role,
|
Role,
|
||||||
getRole,
|
getRole,
|
||||||
|
isBuiltin,
|
||||||
|
getExternalRoleID,
|
||||||
} = require("../../utilities/security/roles")
|
} = require("../../utilities/security/roles")
|
||||||
const {
|
const {
|
||||||
generateRoleID,
|
generateRoleID,
|
||||||
|
@ -16,6 +19,14 @@ const UpdateRolesOptions = {
|
||||||
REMOVED: "removed",
|
REMOVED: "removed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exclude internal roles like builder
|
||||||
|
const EXTERNAL_BUILTIN_ROLE_IDS = [
|
||||||
|
BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
BUILTIN_ROLE_IDS.POWER,
|
||||||
|
BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
BUILTIN_ROLE_IDS.PUBLIC,
|
||||||
|
]
|
||||||
|
|
||||||
async function updateRolesOnUserTable(db, roleId, updateOption) {
|
async function updateRolesOnUserTable(db, roleId, updateOption) {
|
||||||
const table = await db.get(ViewNames.USERS)
|
const table = await db.get(ViewNames.USERS)
|
||||||
const schema = table.schema
|
const schema = table.schema
|
||||||
|
@ -46,16 +57,22 @@ exports.fetch = async function(ctx) {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const customRoles = body.rows.map(row => row.doc)
|
const roles = body.rows.map(row => row.doc)
|
||||||
|
|
||||||
// exclude internal roles like builder
|
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||||
const staticRoles = [
|
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
||||||
BUILTIN_ROLES.ADMIN,
|
const builtinRole = BUILTIN_ROLES[builtinRoleId]
|
||||||
BUILTIN_ROLES.POWER,
|
const dbBuiltin = roles.filter(
|
||||||
BUILTIN_ROLES.BASIC,
|
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
|
||||||
BUILTIN_ROLES.PUBLIC,
|
)[0]
|
||||||
]
|
if (dbBuiltin == null) {
|
||||||
ctx.body = [...staticRoles, ...customRoles]
|
roles.push(builtinRole)
|
||||||
|
} else {
|
||||||
|
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
|
||||||
|
roles.push(Object.assign(builtinRole, dbBuiltin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.body = roles
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
|
@ -67,6 +84,8 @@ exports.save = async function(ctx) {
|
||||||
let { _id, name, inherits, permissionId } = ctx.request.body
|
let { _id, name, inherits, permissionId } = ctx.request.body
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
_id = generateRoleID()
|
_id = generateRoleID()
|
||||||
|
} else if (isBuiltin(_id)) {
|
||||||
|
ctx.throw(400, "Cannot update builtin roles.")
|
||||||
}
|
}
|
||||||
const role = new Role(_id, name)
|
const role = new Role(_id, name)
|
||||||
.addPermission(permissionId)
|
.addPermission(permissionId)
|
||||||
|
@ -84,6 +103,9 @@ exports.save = async function(ctx) {
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.appId)
|
const db = new CouchDB(ctx.user.appId)
|
||||||
const roleId = ctx.params.roleId
|
const roleId = ctx.params.roleId
|
||||||
|
if (isBuiltin(roleId)) {
|
||||||
|
ctx.throw(400, "Cannot delete builtin role.")
|
||||||
|
}
|
||||||
// first check no users actively attached to role
|
// first check no users actively attached to role
|
||||||
const users = (
|
const users = (
|
||||||
await db.allDocs(
|
await db.allDocs(
|
||||||
|
@ -94,7 +116,7 @@ exports.destroy = async function(ctx) {
|
||||||
).rows.map(row => row.doc)
|
).rows.map(row => row.doc)
|
||||||
const usersWithRole = users.filter(user => user.roleId === roleId)
|
const usersWithRole = users.filter(user => user.roleId === roleId)
|
||||||
if (usersWithRole.length !== 0) {
|
if (usersWithRole.length !== 0) {
|
||||||
ctx.throw("Cannot delete role when it is in use.")
|
ctx.throw(400, "Cannot delete role when it is in use.")
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.remove(roleId, ctx.params.rev)
|
await db.remove(roleId, ctx.params.rev)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ router.use(async (ctx, next) => {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
status: ctx.status,
|
status: ctx.status,
|
||||||
}
|
}
|
||||||
|
console.trace(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,10 +1,47 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/permission")
|
const controller = require("../controllers/permission")
|
||||||
const authorized = require("../../middleware/authorized")
|
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()
|
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
|
module.exports = router
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/role")
|
const controller = require("../controllers/role")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const { BUILDER } = require("../../utilities/security/permissions")
|
const {
|
||||||
|
BUILDER,
|
||||||
|
PermissionLevels,
|
||||||
|
} = require("../../utilities/security/permissions")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
const joiValidator = require("../../middleware/joi-validator")
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
const {
|
const {
|
||||||
|
@ -11,12 +14,17 @@ const {
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
function generateValidator() {
|
function generateValidator() {
|
||||||
|
const permLevelArray = Object.values(PermissionLevels)
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
_id: Joi.string().optional(),
|
_id: Joi.string().optional(),
|
||||||
_rev: Joi.string().optional(),
|
_rev: Joi.string().optional(),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
|
// this is the base permission ID (for now a built in)
|
||||||
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(),
|
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(),
|
||||||
|
permissions: Joi.object()
|
||||||
|
.pattern(/.*/, [Joi.string().valid(...permLevelArray)])
|
||||||
|
.optional(),
|
||||||
inherits: Joi.string().optional(),
|
inherits: Joi.string().optional(),
|
||||||
}).unknown(true))
|
}).unknown(true))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,21 +16,25 @@ 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
|
||||||
|
@ -37,17 +45,20 @@ router
|
||||||
rowController.search
|
rowController.search
|
||||||
)
|
)
|
||||||
.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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,9 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
|
||||||
const packageJson = require("../../../../package")
|
const packageJson = require("../../../../package")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
|
const {
|
||||||
|
BUILTIN_PERMISSION_IDS,
|
||||||
|
} = require("../../../utilities/security/permissions")
|
||||||
|
|
||||||
const TEST_CLIENT_ID = "test-client-id"
|
const TEST_CLIENT_ID = "test-client-id"
|
||||||
|
|
||||||
|
@ -37,6 +40,17 @@ exports.defaultHeaders = appId => {
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.publicHeaders = appId => {
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/json",
|
||||||
|
}
|
||||||
|
if (appId) {
|
||||||
|
headers["x-budibase-app-id"] = appId
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
exports.BASE_TABLE = {
|
exports.BASE_TABLE = {
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
type: "table",
|
type: "table",
|
||||||
|
@ -70,6 +84,56 @@ exports.createTable = async (request, appId, table, removeId = true) => {
|
||||||
return res.body
|
return res.body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.makeBasicRow = tableId => {
|
||||||
|
return {
|
||||||
|
name: "Test Contact",
|
||||||
|
description: "original description",
|
||||||
|
status: "new",
|
||||||
|
tableId: tableId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createRow = async (request, appId, tableId, row = null) => {
|
||||||
|
row = row || exports.makeBasicRow(tableId)
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/${tableId}/rows`)
|
||||||
|
.send(row)
|
||||||
|
.set(exports.defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createRole = async (request, appId) => {
|
||||||
|
const roleBody = {
|
||||||
|
name: "NewRole",
|
||||||
|
inherits: BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY,
|
||||||
|
}
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/roles`)
|
||||||
|
.send(roleBody)
|
||||||
|
.set(exports.defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addPermission = async (
|
||||||
|
request,
|
||||||
|
appId,
|
||||||
|
role,
|
||||||
|
resource,
|
||||||
|
level = "read"
|
||||||
|
) => {
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/permission/${role}/${resource}/${level}`)
|
||||||
|
.set(exports.defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
exports.createLinkedTable = async (request, appId) => {
|
exports.createLinkedTable = async (request, appId) => {
|
||||||
// get the ID to link to
|
// get the ID to link to
|
||||||
const table = await exports.createTable(request, appId)
|
const table = await exports.createTable(request, appId)
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,7 +1,5 @@
|
||||||
const {
|
const {
|
||||||
createApplication,
|
createApplication,
|
||||||
createTable,
|
|
||||||
createView,
|
|
||||||
supertest,
|
supertest,
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
} = require("./couchTestUtils")
|
} = require("./couchTestUtils")
|
||||||
|
@ -20,8 +18,6 @@ describe("/roles", () => {
|
||||||
let server
|
let server
|
||||||
let request
|
let request
|
||||||
let appId
|
let appId
|
||||||
let table
|
|
||||||
let view
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;({ request, server } = await supertest())
|
;({ request, server } = await supertest())
|
||||||
|
@ -34,8 +30,6 @@ describe("/roles", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
let app = await createApplication(request)
|
let app = await createApplication(request)
|
||||||
appId = app.instance._id
|
appId = app.instance._id
|
||||||
table = await createTable(request, appId)
|
|
||||||
view = await createView(request, appId, table._id)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
|
|
|
@ -5,6 +5,7 @@ const {
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
createLinkedTable,
|
createLinkedTable,
|
||||||
createAttachmentTable,
|
createAttachmentTable,
|
||||||
|
makeBasicRow,
|
||||||
} = require("./couchTestUtils");
|
} = require("./couchTestUtils");
|
||||||
const { enrichRows } = require("../../../utilities")
|
const { enrichRows } = require("../../../utilities")
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
|
@ -30,12 +31,7 @@ describe("/rows", () => {
|
||||||
app = await createApplication(request)
|
app = await createApplication(request)
|
||||||
appId = app.instance._id
|
appId = app.instance._id
|
||||||
table = await createTable(request, appId)
|
table = await createTable(request, appId)
|
||||||
row = {
|
row = makeBasicRow(table._id)
|
||||||
name: "Test Contact",
|
|
||||||
description: "original description",
|
|
||||||
status: "new",
|
|
||||||
tableId: table._id
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const createRow = async r =>
|
const createRow = async r =>
|
||||||
|
|
|
@ -170,8 +170,8 @@ exports.getAppParams = (appId = null, otherProps = {}) => {
|
||||||
* Generates a new role ID.
|
* Generates a new role ID.
|
||||||
* @returns {string} The new role ID which the role doc can be stored under.
|
* @returns {string} The new role ID which the role doc can be stored under.
|
||||||
*/
|
*/
|
||||||
exports.generateRoleID = () => {
|
exports.generateRoleID = id => {
|
||||||
return `${DocumentTypes.ROLE}${SEPARATOR}${newid()}`
|
return `${DocumentTypes.ROLE}${SEPARATOR}${id || newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
const {
|
const {
|
||||||
BUILTIN_ROLE_IDS,
|
BUILTIN_ROLE_IDS,
|
||||||
getUserPermissionIds,
|
getUserPermissions,
|
||||||
} = require("../utilities/security/roles")
|
} = require("../utilities/security/roles")
|
||||||
const {
|
const {
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
doesHavePermission,
|
doesHaveResourcePermission,
|
||||||
|
doesHaveBasePermission,
|
||||||
} = require("../utilities/security/permissions")
|
} = require("../utilities/security/permissions")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const { isAPIKeyValid } = require("../utilities/security/apikey")
|
const { isAPIKeyValid } = require("../utilities/security/apikey")
|
||||||
|
@ -14,6 +15,10 @@ 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("|"))
|
||||||
|
|
||||||
|
function hasResource(ctx) {
|
||||||
|
return ctx.resourceId != null
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
// webhooks can pass locally
|
// webhooks can pass locally
|
||||||
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
|
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
|
||||||
|
@ -38,25 +43,39 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
// don't expose builder endpoints in the cloud
|
// don't expose builder endpoints in the cloud
|
||||||
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
|
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
|
||||||
|
|
||||||
if (!ctx.auth.authenticated) {
|
|
||||||
ctx.throw(403, "Session not authenticated")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.user) {
|
if (!ctx.user) {
|
||||||
ctx.throw(403, "User not found")
|
ctx.throw(403, "No user info found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = ctx.user.role
|
const role = ctx.user.role
|
||||||
const permissions = await getUserPermissionIds(ctx.appId, role._id)
|
const { basePermissions, permissions } = await getUserPermissions(
|
||||||
if (ADMIN_ROLES.indexOf(role._id) !== -1) {
|
ctx.appId,
|
||||||
return next()
|
role._id
|
||||||
}
|
)
|
||||||
|
const isAdmin = ADMIN_ROLES.indexOf(role._id) !== -1
|
||||||
|
const isAuthed = ctx.auth.authenticated
|
||||||
|
|
||||||
if (permType === PermissionTypes.BUILDER) {
|
// this may need to change in the future, right now only admins
|
||||||
|
// can have access to builder features, this is hard coded into
|
||||||
|
// our rules
|
||||||
|
if (isAdmin && isAuthed) {
|
||||||
|
return next()
|
||||||
|
} else if (permType === PermissionTypes.BUILDER) {
|
||||||
ctx.throw(403, "Not Authorized")
|
ctx.throw(403, "Not Authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doesHavePermission(permType, permLevel, permissions)) {
|
if (
|
||||||
|
hasResource(ctx) &&
|
||||||
|
doesHaveResourcePermission(permissions, permLevel, ctx)
|
||||||
|
) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthed) {
|
||||||
|
ctx.throw(403, "Session not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doesHaveBasePermission(permType, permLevel, basePermissions)) {
|
||||||
ctx.throw(403, "User does not have permission")
|
ctx.throw(403, "User does not have permission")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -94,6 +94,7 @@ exports.getDeployedApps = async () => {
|
||||||
}
|
}
|
||||||
const workerUrl = !env.CLOUD ? await exports.getWorkerUrl() : env.WORKER_URL
|
const workerUrl = !env.CLOUD ? await exports.getWorkerUrl() : env.WORKER_URL
|
||||||
const hostingKey = !env.CLOUD ? hostingInfo.selfHostKey : env.HOSTING_KEY
|
const hostingKey = !env.CLOUD ? hostingInfo.selfHostKey : env.HOSTING_KEY
|
||||||
|
try {
|
||||||
const response = await fetch(`${workerUrl}/api/apps`, {
|
const response = await fetch(`${workerUrl}/api/apps`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -107,4 +108,8 @@ exports.getDeployedApps = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
|
} catch (err) {
|
||||||
|
// error, cannot determine deployed apps, don't stop app creation - sort this later
|
||||||
|
return {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ const PermissionLevels = {
|
||||||
ADMIN: "admin",
|
ADMIN: "admin",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// these are the global types, that govern the underlying default behaviour
|
||||||
const PermissionTypes = {
|
const PermissionTypes = {
|
||||||
TABLE: "table",
|
TABLE: "table",
|
||||||
USER: "user",
|
USER: "user",
|
||||||
|
@ -29,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,
|
||||||
|
@ -97,7 +97,35 @@ exports.BUILTIN_PERMISSIONS = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.doesHavePermission = (permType, permLevel, permissionIds) => {
|
exports.doesHaveResourcePermission = (
|
||||||
|
permissions,
|
||||||
|
permLevel,
|
||||||
|
{ resourceId, subResourceId }
|
||||||
|
) => {
|
||||||
|
// set foundSub to not subResourceId, incase there is no subResource
|
||||||
|
let foundMain = false,
|
||||||
|
foundSub = !subResourceId
|
||||||
|
for (let [resource, level] of Object.entries(permissions)) {
|
||||||
|
const levels = getAllowedLevels(level)
|
||||||
|
if (resource === resourceId && levels.indexOf(permLevel) !== -1) {
|
||||||
|
foundMain = true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
subResourceId &&
|
||||||
|
resource === subResourceId &&
|
||||||
|
levels.indexOf(permLevel) !== -1
|
||||||
|
) {
|
||||||
|
foundSub = true
|
||||||
|
}
|
||||||
|
// this will escape if foundMain only when no sub resource
|
||||||
|
if (foundMain && foundSub) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundMain && foundSub
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
|
||||||
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
|
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
|
||||||
let permissions = flatten(
|
let permissions = flatten(
|
||||||
builtins
|
builtins
|
||||||
|
@ -115,6 +143,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
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
const { BUILTIN_PERMISSION_IDS, higherPermission } = require("./permissions")
|
||||||
|
const { generateRoleID, DocumentTypes, SEPARATOR } = require("../../db/utils")
|
||||||
|
|
||||||
const BUILTIN_IDS = {
|
const BUILTIN_IDS = {
|
||||||
ADMIN: "ADMIN",
|
ADMIN: "ADMIN",
|
||||||
|
@ -44,15 +45,15 @@ exports.BUILTIN_ROLES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
||||||
level => level._id
|
role => role._id
|
||||||
)
|
)
|
||||||
|
|
||||||
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
||||||
level => level.name
|
role => role.name
|
||||||
)
|
)
|
||||||
|
|
||||||
function isBuiltin(role) {
|
function isBuiltin(role) {
|
||||||
return exports.BUILTIN_ROLE_ID_ARRAY.indexOf(role) !== -1
|
return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,14 +67,25 @@ exports.getRole = async (appId, roleId) => {
|
||||||
if (!roleId) {
|
if (!roleId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let role
|
let role = {}
|
||||||
|
// built in roles mostly come from the in-code implementation,
|
||||||
|
// but can be extended by a doc stored about them (e.g. permissions)
|
||||||
if (isBuiltin(roleId)) {
|
if (isBuiltin(roleId)) {
|
||||||
role = cloneDeep(
|
role = cloneDeep(
|
||||||
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
|
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
|
try {
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
role = await db.get(roleId)
|
const dbRole = await db.get(exports.getDBRoleID(roleId))
|
||||||
|
role = Object.assign(role, dbRole)
|
||||||
|
// finalise the ID
|
||||||
|
role._id = exports.getExternalRoleID(role._id)
|
||||||
|
} catch (err) {
|
||||||
|
// only throw an error if there is no role at all
|
||||||
|
if (Object.keys(role).length === 0) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
|
@ -118,14 +130,26 @@ exports.getUserRoleHierarchy = async (appId, userRoleId) => {
|
||||||
* Get all of the user permissions which could be found across the role hierarchy
|
* 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 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.
|
* @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.
|
* @returns {Promise<{basePermissions: string[], permissions: Object}>} the base
|
||||||
|
* permission IDs as well as any custom resource permissions.
|
||||||
*/
|
*/
|
||||||
exports.getUserPermissionIds = async (appId, userRoleId) => {
|
exports.getUserPermissions = async (appId, userRoleId) => {
|
||||||
return [
|
const rolesHierarchy = await getAllUserRoles(appId, userRoleId)
|
||||||
...new Set(
|
const basePermissions = [
|
||||||
(await getAllUserRoles(appId, userRoleId)).map(role => role.permissionId)
|
...new Set(rolesHierarchy.map(role => role.permissionId)),
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
const permissions = {}
|
||||||
|
for (let role of rolesHierarchy) {
|
||||||
|
if (role.permissions) {
|
||||||
|
for (let [resource, level] of Object.entries(role.permissions)) {
|
||||||
|
permissions[resource] = higherPermission(permissions[resource], level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
basePermissions,
|
||||||
|
permissions,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccessController {
|
class AccessController {
|
||||||
|
@ -177,6 +201,27 @@ class AccessController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions).
|
||||||
|
*/
|
||||||
|
exports.getDBRoleID = roleId => {
|
||||||
|
if (roleId.startsWith(DocumentTypes.ROLE)) {
|
||||||
|
return roleId
|
||||||
|
}
|
||||||
|
return generateRoleID(roleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the "role_" from builtin role IDs that have been written to the DB (for permissions).
|
||||||
|
*/
|
||||||
|
exports.getExternalRoleID = roleId => {
|
||||||
|
// for built in roles we want to remove the DB role ID element (role_)
|
||||||
|
if (roleId.startsWith(DocumentTypes.ROLE) && isBuiltin(roleId)) {
|
||||||
|
return roleId.split(`${DocumentTypes.ROLE}${SEPARATOR}`)[1]
|
||||||
|
}
|
||||||
|
return roleId
|
||||||
|
}
|
||||||
|
|
||||||
exports.AccessController = AccessController
|
exports.AccessController = AccessController
|
||||||
exports.BUILTIN_ROLE_IDS = BUILTIN_IDS
|
exports.BUILTIN_ROLE_IDS = BUILTIN_IDS
|
||||||
exports.isBuiltin = isBuiltin
|
exports.isBuiltin = isBuiltin
|
||||||
|
|
Loading…
Reference in New Issue