diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index cb8d154350..6731aea51c 100644 --- a/packages/builder/src/builderStore/store/backend.js +++ b/packages/builder/src/builderStore/store/backend.js @@ -30,6 +30,7 @@ export const getBackendUiStore = () => { const queries = await queriesResponse.json() const integrationsResponse = await api.get("/api/integrations") const integrations = await integrationsResponse.json() + const permissionLevels = await store.actions.permissions.fetchLevels() store.update(state => { state.selectedDatabase = db @@ -37,6 +38,7 @@ export const getBackendUiStore = () => { state.datasources = datasources state.queries = queries state.integrations = integrations + state.permissionLevels = permissionLevels return state }) }, @@ -328,6 +330,25 @@ export const getBackendUiStore = () => { return response }, }, + permissions: { + fetchLevels: async () => { + const response = await api.get("/api/permission/levels") + const json = await response.json() + return json + }, + forResource: async resourceId => { + const response = await api.get(`/api/permission/${resourceId}`) + const json = await response.json() + return json + }, + save: async ({ role, resource, level }) => { + const response = await api.post( + `/api/permission/${role}/${resource}/${level}` + ) + const json = await response.json() + return json + }, + }, } return store diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 436a3b4dee..577fda62a8 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -5,6 +5,7 @@ import CreateViewButton from "./buttons/CreateViewButton.svelte" import ExportButton from "./buttons/ExportButton.svelte" import EditRolesButton from "./buttons/EditRolesButton.svelte" + import ManageAccessButton from "./buttons/ManageAccessButton.svelte" import * as api from "./api" import Table from "./Table.svelte" import { TableNames } from "constants" @@ -47,6 +48,7 @@ title={isUsersTable ? 'Create New User' : 'Create New Row'} modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} /> + {/if} {#if isUsersTable} diff --git a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte index 2ace2bb338..f875fa8849 100644 --- a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte @@ -6,6 +6,7 @@ import GroupByButton from "./buttons/GroupByButton.svelte" import FilterButton from "./buttons/FilterButton.svelte" import ExportButton from "./buttons/ExportButton.svelte" + import ManageAccessButton from "./buttons/ManageAccessButton.svelte" export let view = {} @@ -53,5 +54,6 @@ {#if view.calculation} {/if} + diff --git a/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte new file mode 100644 index 0000000000..2540267d72 --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte @@ -0,0 +1,43 @@ + + +
+ + + Manage Access + +
+ + + + + diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index c42803a241..bc18b7559c 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -142,7 +142,7 @@ thin text="Use as table display column" /> - + + import { onMount } from "svelte" + import { backendUiStore } from "builderStore" + import { Roles } from "constants/backend" + import api from "builderStore/api" + import { notifier } from "builderStore/store/notifications" + import { Button, Label, Input, Select, Spacer } from "@budibase/bbui" + + export let resourceId + export let permissions + export let onClosed + + async function changePermission(level, role) { + await backendUiStore.actions.permissions.save({ + level, + role, + resource: resourceId, + }) + + // Show updated permissions in UI: REMOVE + permissions = await backendUiStore.actions.permissions.forResource( + resourceId + ) + notifier.success("Updated permissions.") + // TODO: update permissions + // permissions[] + } + + +
+
Who Can Access This Data?
+
+ +
+ +
+ + + {#each Object.keys(permissions) as level} + + + {/each} +
+ + + + +
+ + diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index 80eaf613f8..afb474499b 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -92,3 +92,11 @@ export const HostingTypes = { CLOUD: "cloud", SELF: "self", } + +export const Roles = { + ADMIN: "ADMIN", + POWER: "POWER", + BASIC: "BASIC", + PUBLIC: "PUBLIC", + BUILDER: "BUILDER", +} diff --git a/packages/server/src/api/controllers/permission.js b/packages/server/src/api/controllers/permission.js index 1e6bd1869c..c505332c61 100644 --- a/packages/server/src/api/controllers/permission.js +++ b/packages/server/src/api/controllers/permission.js @@ -1,6 +1,7 @@ const { BUILTIN_PERMISSIONS, PermissionLevels, + isPermissionLevelHigherThanRead, higherPermission, } = require("../../utilities/security/permissions") const { @@ -12,12 +13,34 @@ const { const { getRoleParams } = require("../../db/utils") const CouchDB = require("../../db") const { cloneDeep } = require("lodash/fp") +const { + CURRENTLY_SUPPORTED_LEVELS, + getBasePermissions, +} = require("../../utilities/security/utilities") const PermissionUpdateType = { REMOVE: "remove", ADD: "add", } +const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS + +// quick function to perform a bit of weird logic, make sure fetch calls +// always say a write role also has read permission +function fetchLevelPerms(permissions, level, roleId) { + if (!permissions) { + permissions = {} + } + permissions[level] = roleId + if ( + isPermissionLevelHigherThanRead(level) && + !permissions[PermissionLevels.READ] + ) { + permissions[PermissionLevels.READ] = roleId + } + return permissions +} + // utility function to stop this repetition - permissions always stored under roles async function getAllDBRoles(db) { const body = await db.allDocs( @@ -65,7 +88,10 @@ async function updatePermissionOnRole( } // handle the adding, we're on the correct role, at it to this if (!remove && role._id === dbRoleId) { - rolePermissions[resourceId] = level + rolePermissions[resourceId] = higherPermission( + rolePermissions[resourceId], + level + ) updated = true } // handle the update, add it to bulk docs to perform at end @@ -89,7 +115,7 @@ exports.fetchBuiltin = function(ctx) { exports.fetchLevels = function(ctx) { // for now only provide the read/write perms externally - ctx.body = [PermissionLevels.WRITE, PermissionLevels.READ] + ctx.body = SUPPORTED_LEVELS } exports.fetch = async function(ctx) { @@ -98,20 +124,25 @@ exports.fetch = async function(ctx) { 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 - ) - } + if (!role.permissions) { + continue + } + const roleId = getExternalRoleID(role._id) + for (let [resource, level] of Object.entries(role.permissions)) { + permissions[resource] = fetchLevelPerms( + permissions[resource], + level, + roleId + ) } } - ctx.body = permissions + // apply the base permissions + const finalPermissions = {} + for (let [resource, permission] of Object.entries(permissions)) { + const basePerms = getBasePermissions(resource) + finalPermissions[resource] = Object.assign(basePerms, permission) + } + ctx.body = finalPermissions } exports.getResourcePerms = async function(ctx) { @@ -123,18 +154,20 @@ exports.getResourcePerms = async function(ctx) { }) ) const roles = body.rows.map(row => row.doc) - const resourcePerms = {} - for (let role of roles) { + let permissions = {} + for (let level of SUPPORTED_LEVELS) { // 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] - ) + for (let role of roles) { + if (role.permissions && role.permissions[resourceId] === level) { + permissions = fetchLevelPerms( + permissions, + level, + getExternalRoleID(role._id) + ) + } } } - ctx.body = resourcePerms + ctx.body = Object.assign(getBasePermissions(resourceId), permissions) } exports.addPermission = async function(ctx) { diff --git a/packages/server/src/api/controllers/role.js b/packages/server/src/api/controllers/role.js index 59afcc06de..440dbfde35 100644 --- a/packages/server/src/api/controllers/role.js +++ b/packages/server/src/api/controllers/role.js @@ -57,7 +57,7 @@ exports.fetch = async function(ctx) { include_docs: true, }) ) - const roles = body.rows.map(row => row.doc) + let roles = body.rows.map(row => row.doc) // need to combine builtin with any DB record of them (for sake of permissions) for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) { @@ -68,6 +68,8 @@ exports.fetch = async function(ctx) { if (dbBuiltin == null) { roles.push(builtinRole) } else { + // remove role and all back after combining with the builtin + roles = roles.filter(role => role._id !== dbBuiltin._id) dbBuiltin._id = getExternalRoleID(dbBuiltin._id) roles.push(Object.assign(builtinRole, dbBuiltin)) } diff --git a/packages/server/src/api/routes/tests/permissions.spec.js b/packages/server/src/api/routes/tests/permissions.spec.js index 8353eb271d..bb1f072efc 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.js +++ b/packages/server/src/api/routes/tests/permissions.spec.js @@ -71,21 +71,22 @@ describe("/permission", () => { .set(defaultHeaders(appId)) .expect("Content-Type", /json/) .expect(200) - expect(res.body[STD_ROLE_ID]).toEqual("read") + expect(res.body["read"]).toEqual(STD_ROLE_ID) + expect(res.body["write"]).toEqual(HIGHER_ROLE_ID) }) 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") + expect(res.body["read"]).toEqual(STD_ROLE_ID) + expect(res.body["write"]).toEqual(HIGHER_ROLE_ID) 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") + expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID) + expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID) }) }) diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js index 0ae12f687c..f6d1a55803 100644 --- a/packages/server/src/api/routes/view.js +++ b/packages/server/src/api/routes/view.js @@ -2,6 +2,7 @@ const Router = require("@koa/router") const viewController = require("../controllers/view") const rowController = require("../controllers/row") const authorized = require("../../middleware/authorized") +const { paramResource } = require("../../middleware/resourceId") const { BUILDER, PermissionTypes, @@ -15,12 +16,14 @@ router .get("/api/views/export", authorized(BUILDER), viewController.exportView) .get( "/api/views/:viewName", + paramResource("viewName"), authorized(PermissionTypes.VIEW, PermissionLevels.READ), rowController.fetchView ) .get("/api/views", authorized(BUILDER), viewController.fetch) .delete( "/api/views/:viewName", + paramResource("viewName"), authorized(BUILDER), usage, viewController.destroy diff --git a/packages/server/src/utilities/security/permissions.js b/packages/server/src/utilities/security/permissions.js index c0bc26cb8f..342654f9ba 100644 --- a/packages/server/src/utilities/security/permissions.js +++ b/packages/server/src/utilities/security/permissions.js @@ -23,6 +23,22 @@ function Permission(type, level) { this.type = type } +function levelToNumber(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 + } +} + /** * Given the specified permission level for the user return the levels they are allowed to carry out. * @param {string} userPermLevel The permission level of the user. @@ -47,6 +63,7 @@ function getAllowedLevels(userPermLevel) { } exports.BUILTIN_PERMISSION_IDS = { + PUBLIC: "public", READ_ONLY: "read_only", WRITE: "write", ADMIN: "admin", @@ -54,6 +71,13 @@ exports.BUILTIN_PERMISSION_IDS = { } exports.BUILTIN_PERMISSIONS = { + PUBLIC: { + _id: exports.BUILTIN_PERMISSION_IDS.PUBLIC, + name: "Public", + permissions: [ + new Permission(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE), + ], + }, READ_ONLY: { _id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY, name: "Read only", @@ -97,6 +121,11 @@ exports.BUILTIN_PERMISSIONS = { }, } +exports.getBuiltinPermissionByID = id => { + const perms = Object.values(exports.BUILTIN_PERMISSIONS) + return perms.find(perm => perm._id === id) +} + exports.doesHaveResourcePermission = ( permissions, permLevel, @@ -144,22 +173,11 @@ exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => { } 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 + return levelToNumber(perm1) > levelToNumber(perm2) ? perm1 : perm2 +} + +exports.isPermissionLevelHigherThanRead = level => { + return levelToNumber(level) > 1 } // utility as a lot of things need simply the builder permission diff --git a/packages/server/src/utilities/security/roles.js b/packages/server/src/utilities/security/roles.js index 447c049e1d..660f190d6f 100644 --- a/packages/server/src/utilities/security/roles.js +++ b/packages/server/src/utilities/security/roles.js @@ -37,7 +37,7 @@ exports.BUILTIN_ROLES = { .addPermission(BUILTIN_PERMISSION_IDS.WRITE) .addInheritance(BUILTIN_IDS.PUBLIC), PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission( - BUILTIN_PERMISSION_IDS.READ_ONLY + BUILTIN_PERMISSION_IDS.PUBLIC ), BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission( BUILTIN_PERMISSION_IDS.ADMIN @@ -56,6 +56,41 @@ function isBuiltin(role) { return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin)) } +/** + * Works through the inheritance ranks to see how far up the builtin stack this ID is. + */ +function builtinRoleToNumber(id) { + const MAX = Object.values(BUILTIN_IDS).length + 1 + if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { + return MAX + } + let role = exports.BUILTIN_ROLES[id], + count = 0 + do { + if (!role) { + break + } + role = exports.BUILTIN_ROLES[role.inherits] + count++ + } while (role !== null) + return count +} + +/** + * Returns whichever builtin roleID is lower. + */ +exports.lowerBuiltinRoleID = (roleId1, roleId2) => { + if (!roleId1) { + return roleId2 + } + if (!roleId2) { + return roleId1 + } + return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2) + ? roleId2 + : roleId1 +} + /** * Gets the role object, this is mainly useful for two purposes, to check if the level exists and * to check if the role inherits any others. diff --git a/packages/server/src/utilities/security/utilities.js b/packages/server/src/utilities/security/utilities.js new file mode 100644 index 0000000000..9d191b9572 --- /dev/null +++ b/packages/server/src/utilities/security/utilities.js @@ -0,0 +1,70 @@ +const { + PermissionLevels, + PermissionTypes, + getBuiltinPermissionByID, + isPermissionLevelHigherThanRead, +} = require("../../utilities/security/permissions") +const { + lowerBuiltinRoleID, + BUILTIN_ROLES, +} = require("../../utilities/security/roles") +const { DocumentTypes } = require("../../db/utils") + +const CURRENTLY_SUPPORTED_LEVELS = [ + PermissionLevels.WRITE, + PermissionLevels.READ, +] + +exports.getPermissionType = resourceId => { + const docType = Object.values(DocumentTypes).filter(docType => + resourceId.startsWith(docType) + )[0] + switch (docType) { + case DocumentTypes.TABLE: + case DocumentTypes.ROW: + return PermissionTypes.TABLE + case DocumentTypes.AUTOMATION: + return PermissionTypes.AUTOMATION + case DocumentTypes.WEBHOOK: + return PermissionTypes.WEBHOOK + case DocumentTypes.QUERY: + case DocumentTypes.DATASOURCE: + return PermissionTypes.QUERY + default: + // views don't have an ID, will end up here + return PermissionTypes.VIEW + } +} + +/** + * works out the basic permissions based on builtin roles for a resource, using its ID + * @param resourceId + * @returns {{}} + */ +exports.getBasePermissions = resourceId => { + const type = exports.getPermissionType(resourceId) + const permissions = {} + for (let [roleId, role] of Object.entries(BUILTIN_ROLES)) { + if (!role.permissionId) { + continue + } + const perms = getBuiltinPermissionByID(role.permissionId) + const typedPermission = perms.permissions.find(perm => perm.type === type) + if ( + typedPermission && + CURRENTLY_SUPPORTED_LEVELS.indexOf(typedPermission.level) !== -1 + ) { + const level = typedPermission.level + permissions[level] = lowerBuiltinRoleID(permissions[level], roleId) + if (isPermissionLevelHigherThanRead(level)) { + permissions[PermissionLevels.READ] = lowerBuiltinRoleID( + permissions[PermissionLevels.READ], + roleId + ) + } + } + } + return permissions +} + +exports.CURRENTLY_SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS