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