Merge branch 'develop' into relationship-one-to-many
This commit is contained in:
commit
17e4ff1c60
|
@ -6,9 +6,11 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- develop
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -64,9 +64,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.58.5",
|
"@budibase/bbui": "^1.58.5",
|
||||||
"@budibase/client": "^0.7.6",
|
"@budibase/client": "^0.7.7",
|
||||||
"@budibase/colorpicker": "1.0.1",
|
"@budibase/colorpicker": "1.0.1",
|
||||||
"@budibase/string-templates": "^0.7.6",
|
"@budibase/string-templates": "^0.7.7",
|
||||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@svelteschool/svelte-forms": "0.7.0",
|
"@svelteschool/svelte-forms": "0.7.0",
|
||||||
|
|
|
@ -30,6 +30,7 @@ export const getBackendUiStore = () => {
|
||||||
const queries = await queriesResponse.json()
|
const queries = await queriesResponse.json()
|
||||||
const integrationsResponse = await api.get("/api/integrations")
|
const integrationsResponse = await api.get("/api/integrations")
|
||||||
const integrations = await integrationsResponse.json()
|
const integrations = await integrationsResponse.json()
|
||||||
|
const permissionLevels = await store.actions.permissions.fetchLevels()
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedDatabase = db
|
state.selectedDatabase = db
|
||||||
|
@ -37,6 +38,7 @@ export const getBackendUiStore = () => {
|
||||||
state.datasources = datasources
|
state.datasources = datasources
|
||||||
state.queries = queries
|
state.queries = queries
|
||||||
state.integrations = integrations
|
state.integrations = integrations
|
||||||
|
state.permissionLevels = permissionLevels
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -333,6 +335,25 @@ export const getBackendUiStore = () => {
|
||||||
return response
|
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
|
return store
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||||
import ExportButton from "./buttons/ExportButton.svelte"
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||||
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
import * as api from "./api"
|
import * as api from "./api"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
title={isUsersTable ? 'Create New User' : 'Create New Row'}
|
title={isUsersTable ? 'Create New User' : 'Create New Row'}
|
||||||
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
|
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
|
||||||
<CreateViewButton />
|
<CreateViewButton />
|
||||||
|
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
|
||||||
<ExportButton view={tableView} />
|
<ExportButton view={tableView} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isUsersTable}
|
{#if isUsersTable}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import GroupByButton from "./buttons/GroupByButton.svelte"
|
import GroupByButton from "./buttons/GroupByButton.svelte"
|
||||||
import FilterButton from "./buttons/FilterButton.svelte"
|
import FilterButton from "./buttons/FilterButton.svelte"
|
||||||
import ExportButton from "./buttons/ExportButton.svelte"
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
|
|
||||||
export let view = {}
|
export let view = {}
|
||||||
|
|
||||||
|
@ -53,5 +54,6 @@
|
||||||
{#if view.calculation}
|
{#if view.calculation}
|
||||||
<GroupByButton {view} />
|
<GroupByButton {view} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<ManageAccessButton resourceId={decodeURI(name)} />
|
||||||
<ExportButton {view} />
|
<ExportButton {view} />
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { TextButton, Icon, Popover } from "@budibase/bbui"
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
import ManageAccessPopover from "../popovers/ManageAccessPopover.svelte"
|
||||||
|
|
||||||
|
export let resourceId
|
||||||
|
|
||||||
|
let anchor
|
||||||
|
let dropdown
|
||||||
|
let levels
|
||||||
|
let permissions
|
||||||
|
|
||||||
|
async function openDropdown() {
|
||||||
|
permissions = await backendUiStore.actions.permissions.forResource(
|
||||||
|
resourceId
|
||||||
|
)
|
||||||
|
levels = await backendUiStore.actions.permissions.fetchLevels()
|
||||||
|
dropdown.show()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={anchor}>
|
||||||
|
<TextButton text small on:click={openDropdown}>
|
||||||
|
<i class="ri-lock-line" />
|
||||||
|
Manage Access
|
||||||
|
</TextButton>
|
||||||
|
</div>
|
||||||
|
<Popover bind:this={dropdown} {anchor} align="left">
|
||||||
|
<ManageAccessPopover
|
||||||
|
{resourceId}
|
||||||
|
{levels}
|
||||||
|
{permissions}
|
||||||
|
onClosed={dropdown.hide} />
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
i {
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -145,7 +145,7 @@
|
||||||
thin
|
thin
|
||||||
text="Use as table display column" />
|
text="Use as table display column" />
|
||||||
|
|
||||||
<Label gray small>Search Indexes</Label>
|
<Label grey small>Search Indexes</Label>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={indexes[0] === field.name}
|
checked={indexes[0] === field.name}
|
||||||
disabled={indexes[1] === field.name}
|
disabled={indexes[1] === field.name}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
<script>
|
||||||
|
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[]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="popover">
|
||||||
|
<h5>Who Can Access This Data?</h5>
|
||||||
|
<div class="note">
|
||||||
|
<Label extraSmall grey>Specify the minimum access level role for this data.</Label>
|
||||||
|
</div>
|
||||||
|
<Spacer large />
|
||||||
|
<div class="row">
|
||||||
|
<Label extraSmall grey>Level</Label>
|
||||||
|
<Label extraSmall grey>Role</Label>
|
||||||
|
{#each Object.keys(permissions) as level}
|
||||||
|
<Input secondary thin value={level} disabled={true} />
|
||||||
|
<Select
|
||||||
|
secondary
|
||||||
|
thin
|
||||||
|
value={permissions[level]}
|
||||||
|
on:change={e => changePermission(level, e.target.value)}>
|
||||||
|
{#each $backendUiStore.roles as role}
|
||||||
|
<option value={role._id}>{role.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer large />
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popover {
|
||||||
|
display: grid;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: var(--spacing-s) 0 var(--spacing-m) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -92,3 +92,11 @@ export const HostingTypes = {
|
||||||
CLOUD: "cloud",
|
CLOUD: "cloud",
|
||||||
SELF: "self",
|
SELF: "self",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Roles = {
|
||||||
|
ADMIN: "ADMIN",
|
||||||
|
POWER: "POWER",
|
||||||
|
BASIC: "BASIC",
|
||||||
|
PUBLIC: "PUBLIC",
|
||||||
|
BUILDER: "BUILDER",
|
||||||
|
}
|
||||||
|
|
|
@ -48,6 +48,11 @@
|
||||||
modal.show()
|
modal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
template = null
|
||||||
|
modal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
checkIfKeysAndApps()
|
checkIfKeysAndApps()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@
|
||||||
<AppList />
|
<AppList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={modal} padding={false} width="600px">
|
<Modal bind:this={modal} padding={false} width="600px" on:hide={closeModal}>
|
||||||
<CreateAppModal {hasKey} {template} />
|
<CreateAppModal {hasKey} {template} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
|
@ -9,14 +9,14 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/string-templates": "^0.7.6",
|
"@budibase/string-templates": "^0.7.7",
|
||||||
"deep-equal": "^2.0.1",
|
"deep-equal": "^2.0.1",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@budibase/standard-components": "^0.7.6",
|
"@budibase/standard-components": "^0.7.7",
|
||||||
"@rollup/plugin-commonjs": "^16.0.0",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^10.0.0",
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/electron.js",
|
"main": "src/electron.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -50,8 +50,8 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/client": "^0.7.6",
|
"@budibase/client": "^0.7.7",
|
||||||
"@budibase/string-templates": "^0.7.6",
|
"@budibase/string-templates": "^0.7.7",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
"@koa/router": "8.0.0",
|
"@koa/router": "8.0.0",
|
||||||
"@sendgrid/mail": "7.1.1",
|
"@sendgrid/mail": "7.1.1",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const {
|
const {
|
||||||
BUILTIN_PERMISSIONS,
|
BUILTIN_PERMISSIONS,
|
||||||
PermissionLevels,
|
PermissionLevels,
|
||||||
|
isPermissionLevelHigherThanRead,
|
||||||
higherPermission,
|
higherPermission,
|
||||||
} = require("../../utilities/security/permissions")
|
} = require("../../utilities/security/permissions")
|
||||||
const {
|
const {
|
||||||
|
@ -12,12 +13,34 @@ const {
|
||||||
const { getRoleParams } = require("../../db/utils")
|
const { getRoleParams } = require("../../db/utils")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
const {
|
||||||
|
CURRENTLY_SUPPORTED_LEVELS,
|
||||||
|
getBasePermissions,
|
||||||
|
} = require("../../utilities/security/utilities")
|
||||||
|
|
||||||
const PermissionUpdateType = {
|
const PermissionUpdateType = {
|
||||||
REMOVE: "remove",
|
REMOVE: "remove",
|
||||||
ADD: "add",
|
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
|
// utility function to stop this repetition - permissions always stored under roles
|
||||||
async function getAllDBRoles(db) {
|
async function getAllDBRoles(db) {
|
||||||
const body = await db.allDocs(
|
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
|
// handle the adding, we're on the correct role, at it to this
|
||||||
if (!remove && role._id === dbRoleId) {
|
if (!remove && role._id === dbRoleId) {
|
||||||
rolePermissions[resourceId] = level
|
rolePermissions[resourceId] = higherPermission(
|
||||||
|
rolePermissions[resourceId],
|
||||||
|
level
|
||||||
|
)
|
||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
// handle the update, add it to bulk docs to perform at end
|
// handle the update, add it to bulk docs to perform at end
|
||||||
|
@ -89,7 +115,7 @@ exports.fetchBuiltin = function(ctx) {
|
||||||
|
|
||||||
exports.fetchLevels = function(ctx) {
|
exports.fetchLevels = function(ctx) {
|
||||||
// for now only provide the read/write perms externally
|
// for now only provide the read/write perms externally
|
||||||
ctx.body = [PermissionLevels.WRITE, PermissionLevels.READ]
|
ctx.body = SUPPORTED_LEVELS
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
|
@ -98,20 +124,25 @@ exports.fetch = async function(ctx) {
|
||||||
let permissions = {}
|
let permissions = {}
|
||||||
// create an object with structure role ID -> resource ID -> level
|
// create an object with structure role ID -> resource ID -> level
|
||||||
for (let role of roles) {
|
for (let role of roles) {
|
||||||
if (role.permissions) {
|
if (!role.permissions) {
|
||||||
const roleId = getExternalRoleID(role._id)
|
continue
|
||||||
if (permissions[roleId] == null) {
|
|
||||||
permissions[roleId] = {}
|
|
||||||
}
|
}
|
||||||
|
const roleId = getExternalRoleID(role._id)
|
||||||
for (let [resource, level] of Object.entries(role.permissions)) {
|
for (let [resource, level] of Object.entries(role.permissions)) {
|
||||||
permissions[roleId][resource] = higherPermission(
|
permissions[resource] = fetchLevelPerms(
|
||||||
permissions[roleId][resource],
|
permissions[resource],
|
||||||
level
|
level,
|
||||||
|
roleId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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 = permissions
|
ctx.body = finalPermissions
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getResourcePerms = async function(ctx) {
|
exports.getResourcePerms = async function(ctx) {
|
||||||
|
@ -123,18 +154,20 @@ exports.getResourcePerms = async function(ctx) {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const roles = body.rows.map(row => row.doc)
|
const roles = body.rows.map(row => row.doc)
|
||||||
const resourcePerms = {}
|
let permissions = {}
|
||||||
for (let role of roles) {
|
for (let level of SUPPORTED_LEVELS) {
|
||||||
// update the various roleIds in the resource permissions
|
// update the various roleIds in the resource permissions
|
||||||
if (role.permissions && role.permissions[resourceId]) {
|
for (let role of roles) {
|
||||||
const roleId = getExternalRoleID(role._id)
|
if (role.permissions && role.permissions[resourceId] === level) {
|
||||||
resourcePerms[roleId] = higherPermission(
|
permissions = fetchLevelPerms(
|
||||||
resourcePerms[roleId],
|
permissions,
|
||||||
role.permissions[resourceId]
|
level,
|
||||||
|
getExternalRoleID(role._id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.body = resourcePerms
|
}
|
||||||
|
ctx.body = Object.assign(getBasePermissions(resourceId), permissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.addPermission = async function(ctx) {
|
exports.addPermission = async function(ctx) {
|
||||||
|
|
|
@ -57,7 +57,7 @@ exports.fetch = async function(ctx) {
|
||||||
include_docs: true,
|
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)
|
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||||
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
||||||
|
@ -68,6 +68,8 @@ exports.fetch = async function(ctx) {
|
||||||
if (dbBuiltin == null) {
|
if (dbBuiltin == null) {
|
||||||
roles.push(builtinRole)
|
roles.push(builtinRole)
|
||||||
} else {
|
} else {
|
||||||
|
// remove role and all back after combining with the builtin
|
||||||
|
roles = roles.filter(role => role._id !== dbBuiltin._id)
|
||||||
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
|
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
|
||||||
roles.push(Object.assign(builtinRole, dbBuiltin))
|
roles.push(Object.assign(builtinRole, dbBuiltin))
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,21 +71,22 @@ describe("/permission", () => {
|
||||||
.set(defaultHeaders(appId))
|
.set(defaultHeaders(appId))
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.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 () => {
|
it("should get resource permissions with multiple roles", async () => {
|
||||||
perms = await addPermission(request, appId, HIGHER_ROLE_ID, table._id, "write")
|
perms = await addPermission(request, appId, HIGHER_ROLE_ID, table._id, "write")
|
||||||
const res = await getTablePermissions()
|
const res = await getTablePermissions()
|
||||||
expect(res.body[HIGHER_ROLE_ID]).toEqual("write")
|
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||||
expect(res.body[STD_ROLE_ID]).toEqual("read")
|
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
const allRes = await request
|
const allRes = await request
|
||||||
.get(`/api/permission`)
|
.get(`/api/permission`)
|
||||||
.set(defaultHeaders(appId))
|
.set(defaultHeaders(appId))
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(allRes.body[HIGHER_ROLE_ID][table._id]).toEqual("write")
|
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
expect(allRes.body[STD_ROLE_ID][table._id]).toEqual("read")
|
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ const Router = require("@koa/router")
|
||||||
const viewController = require("../controllers/view")
|
const viewController = require("../controllers/view")
|
||||||
const rowController = require("../controllers/row")
|
const rowController = require("../controllers/row")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
|
const { paramResource } = require("../../middleware/resourceId")
|
||||||
const {
|
const {
|
||||||
BUILDER,
|
BUILDER,
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
|
@ -15,12 +16,14 @@ router
|
||||||
.get("/api/views/export", authorized(BUILDER), viewController.exportView)
|
.get("/api/views/export", authorized(BUILDER), viewController.exportView)
|
||||||
.get(
|
.get(
|
||||||
"/api/views/:viewName",
|
"/api/views/:viewName",
|
||||||
|
paramResource("viewName"),
|
||||||
authorized(PermissionTypes.VIEW, PermissionLevels.READ),
|
authorized(PermissionTypes.VIEW, PermissionLevels.READ),
|
||||||
rowController.fetchView
|
rowController.fetchView
|
||||||
)
|
)
|
||||||
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/views/:viewName",
|
"/api/views/:viewName",
|
||||||
|
paramResource("viewName"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
usage,
|
usage,
|
||||||
viewController.destroy
|
viewController.destroy
|
||||||
|
|
|
@ -23,6 +23,22 @@ function Permission(type, level) {
|
||||||
this.type = type
|
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.
|
* 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.
|
* @param {string} userPermLevel The permission level of the user.
|
||||||
|
@ -47,6 +63,7 @@ function getAllowedLevels(userPermLevel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.BUILTIN_PERMISSION_IDS = {
|
exports.BUILTIN_PERMISSION_IDS = {
|
||||||
|
PUBLIC: "public",
|
||||||
READ_ONLY: "read_only",
|
READ_ONLY: "read_only",
|
||||||
WRITE: "write",
|
WRITE: "write",
|
||||||
ADMIN: "admin",
|
ADMIN: "admin",
|
||||||
|
@ -54,6 +71,13 @@ exports.BUILTIN_PERMISSION_IDS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.BUILTIN_PERMISSIONS = {
|
exports.BUILTIN_PERMISSIONS = {
|
||||||
|
PUBLIC: {
|
||||||
|
_id: exports.BUILTIN_PERMISSION_IDS.PUBLIC,
|
||||||
|
name: "Public",
|
||||||
|
permissions: [
|
||||||
|
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE),
|
||||||
|
],
|
||||||
|
},
|
||||||
READ_ONLY: {
|
READ_ONLY: {
|
||||||
_id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY,
|
_id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY,
|
||||||
name: "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 = (
|
exports.doesHaveResourcePermission = (
|
||||||
permissions,
|
permissions,
|
||||||
permLevel,
|
permLevel,
|
||||||
|
@ -144,22 +173,11 @@ exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.higherPermission = (perm1, perm2) => {
|
exports.higherPermission = (perm1, perm2) => {
|
||||||
function toNum(perm) {
|
return levelToNumber(perm1) > levelToNumber(perm2) ? perm1 : perm2
|
||||||
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
|
exports.isPermissionLevelHigherThanRead = level => {
|
||||||
|
return levelToNumber(level) > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// utility as a lot of things need simply the builder permission
|
// utility as a lot of things need simply the builder permission
|
||||||
|
|
|
@ -37,7 +37,7 @@ exports.BUILTIN_ROLES = {
|
||||||
.addPermission(BUILTIN_PERMISSION_IDS.WRITE)
|
.addPermission(BUILTIN_PERMISSION_IDS.WRITE)
|
||||||
.addInheritance(BUILTIN_IDS.PUBLIC),
|
.addInheritance(BUILTIN_IDS.PUBLIC),
|
||||||
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission(
|
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(
|
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission(
|
||||||
BUILTIN_PERMISSION_IDS.ADMIN
|
BUILTIN_PERMISSION_IDS.ADMIN
|
||||||
|
@ -56,6 +56,41 @@ function isBuiltin(role) {
|
||||||
return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin))
|
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
|
* 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.
|
* to check if the role inherits any others.
|
||||||
|
|
|
@ -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
|
|
@ -35,7 +35,7 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
],
|
],
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"module": "src/index.js",
|
"module": "src/index.js",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/deployment",
|
"name": "@budibase/deployment",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"description": "Budibase Deployment Server",
|
"description": "Budibase Deployment Server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
Loading…
Reference in New Issue