diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index bd10833f91..50780b45dd 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -6,9 +6,11 @@ on: push: branches: - master + - develop pull_request: branches: - master + - develop jobs: build: diff --git a/lerna.json b/lerna.json index 5055269980..e45c2a6a5a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.7.6", + "version": "0.7.7", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/builder/package.json b/packages/builder/package.json index 267e4f5d72..85580fa40b 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.7.6", + "version": "0.7.7", "license": "AGPL-3.0", "private": true, "scripts": { @@ -64,9 +64,9 @@ }, "dependencies": { "@budibase/bbui": "^1.58.5", - "@budibase/client": "^0.7.6", + "@budibase/client": "^0.7.7", "@budibase/colorpicker": "1.0.1", - "@budibase/string-templates": "^0.7.6", + "@budibase/string-templates": "^0.7.7", "@budibase/svelte-ag-grid": "^0.0.16", "@sentry/browser": "5.19.1", "@svelteschool/svelte-forms": "0.7.0", diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index 803b5f795f..69c601cf2b 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 }) }, @@ -333,6 +335,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 3e08096152..66317b26ea 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -145,7 +145,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/builder/src/pages/index.svelte b/packages/builder/src/pages/index.svelte index 5aa99be35b..c442607962 100644 --- a/packages/builder/src/pages/index.svelte +++ b/packages/builder/src/pages/index.svelte @@ -48,6 +48,11 @@ modal.show() } + function closeModal() { + template = null + modal.hide() + } + checkIfKeysAndApps() @@ -73,7 +78,7 @@ - + diff --git a/packages/client/package.json b/packages/client/package.json index 8541e2f807..d080bea4b3 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "0.7.6", + "version": "0.7.7", "license": "MPL-2.0", "main": "dist/budibase-client.js", "module": "dist/budibase-client.js", @@ -9,14 +9,14 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/string-templates": "^0.7.6", + "@budibase/string-templates": "^0.7.7", "deep-equal": "^2.0.1", "regexparam": "^1.3.0", "shortid": "^2.2.15", "svelte-spa-router": "^3.0.5" }, "devDependencies": { - "@budibase/standard-components": "^0.7.6", + "@budibase/standard-components": "^0.7.7", "@rollup/plugin-commonjs": "^16.0.0", "@rollup/plugin-node-resolve": "^10.0.0", "fs-extra": "^8.1.0", diff --git a/packages/server/package.json b/packages/server/package.json index 61b6fcf31a..29f6b86a7f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "0.7.6", + "version": "0.7.7", "description": "Budibase Web Server", "main": "src/electron.js", "repository": { @@ -50,8 +50,8 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/client": "^0.7.6", - "@budibase/string-templates": "^0.7.6", + "@budibase/client": "^0.7.7", + "@budibase/string-templates": "^0.7.7", "@elastic/elasticsearch": "7.10.0", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", 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 diff --git a/packages/standard-components/package.json b/packages/standard-components/package.json index cb56664bbd..2eb25f37e8 100644 --- a/packages/standard-components/package.json +++ b/packages/standard-components/package.json @@ -35,7 +35,7 @@ "keywords": [ "svelte" ], - "version": "0.7.6", + "version": "0.7.7", "license": "MIT", "gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd", "dependencies": { diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index fae7314241..5c79020e70 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "0.7.6", + "version": "0.7.7", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.js", "module": "src/index.js", diff --git a/packages/worker/package.json b/packages/worker/package.json index 65e4c4742d..4256b527c2 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/deployment", "email": "hi@budibase.com", - "version": "0.7.6", + "version": "0.7.7", "description": "Budibase Deployment Server", "main": "src/index.js", "repository": {