1
0
Fork 0
mirror of synced 2024-07-04 14:01:27 +12:00

Merge branch 'develop' of github.com:Budibase/budibase into budi-day-02-11-cheeks-joe

This commit is contained in:
Andrew Kingston 2021-02-17 15:25:34 +00:00
commit 09e3ab9078
51 changed files with 1487 additions and 1214 deletions

View file

@ -6,9 +6,11 @@ on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop
jobs:
build:

View file

@ -1,5 +1,5 @@
{
"version": "0.7.6",
"version": "0.7.8",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.7.6",
"version": "0.7.8",
"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.8",
"@budibase/colorpicker": "1.0.1",
"@budibase/string-templates": "^0.7.6",
"@budibase/string-templates": "^0.7.8",
"@budibase/svelte-ag-grid": "^0.0.16",
"@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "0.7.0",

View file

@ -11,21 +11,24 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
/**
* Gets all bindable data context fields and instance fields.
*/
export const getBindableProperties = (rootComponent, componentId) => {
return getContextBindings(rootComponent, componentId)
export const getBindableProperties = (asset, componentId) => {
const contextBindings = getContextBindings(asset, componentId)
const userBindings = getUserBindings()
const urlBindings = getUrlBindings(asset, componentId)
return [...contextBindings, ...userBindings, ...urlBindings]
}
/**
* Gets all data provider components above a component.
*/
export const getDataProviderComponents = (rootComponent, componentId) => {
if (!rootComponent || !componentId) {
export const getDataProviderComponents = (asset, componentId) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
@ -38,18 +41,14 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
/**
* Gets all data provider components above a component.
*/
export const getActionProviderComponents = (
rootComponent,
componentId,
actionType
) => {
if (!rootComponent || !componentId) {
export const getActionProviderComponents = (asset, componentId, actionType) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
@ -92,13 +91,12 @@ export const getDatasourceForProvider = component => {
}
/**
* Gets all bindable data contexts. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
* Gets all bindable data properties from component data contexts.
*/
export const getContextBindings = (rootComponent, componentId) => {
const getContextBindings = (asset, componentId) => {
// Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(rootComponent, componentId)
let contextBindings = []
const dataProviders = getDataProviderComponents(asset, componentId)
let bindings = []
// Create bindings for each data provider
dataProviders.forEach(component => {
@ -109,7 +107,7 @@ export const getContextBindings = (rootComponent, componentId) => {
// Forms are an edge case which do not need table schemas
if (isForm) {
schema = buildFormSchema(component)
tableName = "Schema"
tableName = "Fields"
} else {
if (!datasource) {
return
@ -143,7 +141,7 @@ export const getContextBindings = (rootComponent, componentId) => {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
runtimeBoundKey
@ -157,7 +155,14 @@ export const getContextBindings = (rootComponent, componentId) => {
})
})
// Add logged in user bindings
return bindings
}
/**
* Gets all bindable properties from the logged in user.
*/
const getUserBindings = () => {
let bindings = []
const tables = get(backendUiStore).tables
const userTable = tables.find(table => table._id === TableNames.USERS)
const schema = {
@ -176,7 +181,7 @@ export const getContextBindings = (rootComponent, componentId) => {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `user.${runtimeBoundKey}`,
readableBinding: `Current User.${key}`,
@ -187,7 +192,26 @@ export const getContextBindings = (rootComponent, componentId) => {
})
})
return contextBindings
return bindings
}
/**
* Gets all bindable properties from URL parameters.
*/
const getUrlBindings = asset => {
const url = asset?.routing?.route ?? ""
const split = url.split("/")
let params = []
split.forEach(part => {
if (part.startsWith(":") && part.length > 1) {
params.push(part.replace(/:/g, "").replace(/\?/g, ""))
}
})
return params.map(param => ({
type: "context",
runtimeBinding: `url.${param}`,
readableBinding: `URL.${param}`,
}))
}
/**

View file

@ -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

View file

@ -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} />
<CreateViewButton />
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
<ExportButton view={tableView} />
{/if}
{#if isUsersTable}

View file

@ -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}
<GroupByButton {view} />
{/if}
<ManageAccessButton resourceId={decodeURI(name)} />
<ExportButton {view} />
</Table>

View file

@ -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>

View file

@ -142,7 +142,7 @@
thin
text="Use as table display column" />
<Label gray small>Search Indexes</Label>
<Label grey small>Search Indexes</Label>
<Toggle
checked={indexes[0] === field.name}
disabled={indexes[1] === field.name}

View file

@ -0,0 +1,94 @@
<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>

View file

@ -0,0 +1,84 @@
<script>
import { Icon, Input, Drawer, Body, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value = ""
export let bindings = []
let bindingDrawer
let tempValue = value
$: readableValue = runtimeToReadableBinding(bindings, value)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
dispatch("change", readableToRuntimeBinding(bindings, value))
}
</script>
<div class="control">
<Input
thin
value={readableValue}
on:change={event => onChange(event.target.value)}
placeholder="/screen" />
<div class="icon" on:click={bindingDrawer.show}>
<Icon name="lightning" />
</div>
</div>
<Drawer bind:this={bindingDrawer} title="Bindings">
<div slot="description">
<Body extraSmall grey>
Add the objects on the left to enrich your text.
</Body>
</div>
<heading slot="buttons">
<Button thin blue on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
value={readableValue}
close={handleClose}
on:update={event => (tempValue = event.detail)}
bindableProperties={bindings} />
</div>
</Drawer>
<style>
.control {
flex: 1;
margin-left: var(--spacing-l);
position: relative;
}
.icon {
right: 2px;
top: 2px;
bottom: 2px;
position: absolute;
align-items: center;
display: flex;
box-sizing: border-box;
padding-left: 7px;
border-left: 1px solid var(--grey-4);
background-color: var(--grey-2);
border-top-right-radius: var(--border-radius-m);
border-bottom-right-radius: var(--border-radius-m);
color: var(--grey-7);
font-size: 14px;
}
.icon:hover {
color: var(--ink);
cursor: pointer;
}
</style>

View file

@ -24,7 +24,7 @@
$: value && checkValid()
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: dispatch("update", value)

View file

@ -47,7 +47,7 @@
type: "query",
}))
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: queryBindableProperties = bindableProperties.map(property => ({

View file

@ -10,7 +10,7 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: {

View file

@ -10,7 +10,7 @@
ds => ds._id === parameters.datasourceId
)
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
).map(property => ({
...property,

View file

@ -1,18 +1,23 @@
<script>
import { DataList, Label } from "@budibase/bbui"
import { allScreens } from "builderStore"
import { Label } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
export let parameters
let bindingDrawer
let tempValue = parameters.url
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<div class="root">
<Label size="m" color="dark">Screen</Label>
<DataList secondary bind:value={parameters.url}>
<option value="" />
{#each $allScreens as screen}
<option value={screen.routing.route}>{screen.props._instanceName}</option>
{/each}
</DataList>
<DrawerBindableInput
value={parameters.url}
on:change={value => (parameters.url = value.detail)}
{bindings} />
</div>
<style>

View file

@ -6,7 +6,7 @@
export let parameters
$: dataProviders = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
</script>

View file

@ -25,7 +25,7 @@
const emptyField = () => ({ name: "", value: "" })
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)

View file

@ -11,7 +11,7 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: providerComponent = dataProviderComponents.find(

View file

@ -6,7 +6,7 @@
export let parameters
$: actionProviders = getActionProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId,
"ValidateForm"
)

View file

@ -24,7 +24,7 @@
let valid
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)

View file

@ -1,80 +0,0 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, allScreens, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
export let value = ""
$: urls = getUrls($allScreens, $currentAsset, $store.selectedComponentId)
// Update value on blur
const dispatch = createEventDispatcher()
const handleBlur = () => dispatch("change", value)
// Get all valid screen URL, as well as detail screens which can be used in
// the current data context
const getUrls = (screens, asset, componentId) => {
// Get all screens which aren't detail screens
let urls = screens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.routing.route,
sort: screen.props._component,
}))
// Add detail screens enriched with the current data context
const bindableProperties = getBindableProperties(asset.props, componentId)
screens
.filter(screen => screen.props._component.endsWith("/rowdetail"))
.forEach(detailScreen => {
// Find any _id bindings that match the detail screen's table
const binding = bindableProperties.find(p => {
return (
p.type === "context" &&
p.runtimeBinding.endsWith("._id") &&
p.tableId === detailScreen.props.table
)
})
if (binding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.routing.route.replace(
":id",
`{{ ${binding.runtimeBinding} }}`
),
sort: detailScreen.props._component,
})
}
})
return urls
}
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each urls as url}
<option value={url.url}>{url.name}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View file

@ -19,7 +19,6 @@
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor"
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
@ -63,7 +62,6 @@
text: Input,
select: OptionSelect,
datasource: DatasourceSelect,
screen: ScreenSelect,
detailScreen: DetailScreenSelect,
boolean: Checkbox,
number: Input,

View file

@ -92,3 +92,11 @@ export const HostingTypes = {
CLOUD: "cloud",
SELF: "self",
}
export const Roles = {
ADMIN: "ADMIN",
POWER: "POWER",
BASIC: "BASIC",
PUBLIC: "PUBLIC",
BUILDER: "BUILDER",
}

View file

@ -48,6 +48,11 @@
modal.show()
}
function closeModal() {
template = null
modal.hide()
}
checkIfKeysAndApps()
</script>
@ -73,7 +78,7 @@
<AppList />
</div>
<Modal bind:this={modal} padding={false} width="600px">
<Modal bind:this={modal} padding={false} width="600px" on:hide={closeModal}>
<CreateAppModal {hasKey} {template} />
</Modal>

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.7.6",
"version": "0.7.8",
"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.8",
"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.8",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"fs-extra": "^8.1.0",

View file

@ -1,5 +1,5 @@
<script>
import { getContext, setContext } from "svelte"
import { getContext } from "svelte"
import Router from "svelte-spa-router"
import { routeStore } from "../store"
import Screen from "./Screen.svelte"
@ -10,12 +10,10 @@
// Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config
// changes
$: configs = [
{
routes: getRouterConfig($routeStore.routes),
id: $routeStore.routeSessionId,
},
]
$: config = {
routes: getRouterConfig($routeStore.routes),
id: $routeStore.routeSessionId,
}
const getRouterConfig = routes => {
let config = {}
@ -33,11 +31,11 @@
}
</script>
{#each configs as config (config.id)}
{#key config.id}
<div use:styleable={$component.styles}>
<Router on:routeLoading={onRouteLoading} routes={config.routes} />
</div>
{/each}
{/key}
<style>
div {

View file

@ -2,6 +2,7 @@
import { fade } from "svelte/transition"
import { screenStore, routeStore } from "../store"
import Component from "./Component.svelte"
import Provider from "./Provider.svelte"
// Keep route params up to date
export let params = {}
@ -12,16 +13,16 @@
// Redirect to home layout if no matching route
$: screenDefinition == null && routeStore.actions.navigate("/")
// Make a screen array so we can use keying to properly re-render each screen
$: screens = screenDefinition ? [screenDefinition] : []
</script>
{#each screens as screen (screen._id)}
<div in:fade>
<Component definition={screen} />
</div>
{/each}
<!-- Ensure to fully remount when screen changes -->
{#key screenDefinition?._id}
<Provider key="url" data={params}>
<div in:fade>
<Component definition={screenDefinition} />
</div>
</Provider>
{/key}
<style>
div {

View file

@ -44,7 +44,7 @@ export const enrichProps = async (props, context) => {
let enrichedProps = await enrichDataBindings(validProps, totalContext)
// Enrich button actions if they exist
if (props._component.endsWith("/button") && enrichedProps.onClick) {
if (props._component?.endsWith("/button") && enrichedProps.onClick) {
enrichedProps.onClick = enrichButtonActions(
enrichedProps.onClick,
totalContext

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.7.6",
"version": "0.7.8",
"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.8",
"@budibase/string-templates": "^0.7.8",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",

View file

@ -14,7 +14,6 @@ exports.fetchInfo = async ctx => {
}
exports.save = async ctx => {
console.trace("DID A SAVE!")
const db = new CouchDB(BUILDER_CONFIG_DB)
const { type } = ctx.request.body
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {

View file

@ -1,23 +1,45 @@
const {
BUILTIN_PERMISSIONS,
getBuiltinPermissions,
PermissionLevels,
isPermissionLevelHigherThanRead,
higherPermission,
} = require("../../utilities/security/permissions")
const {
isBuiltin,
getDBRoleID,
getExternalRoleID,
BUILTIN_ROLES,
getBuiltinRoles,
} = require("../../utilities/security/roles")
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(
@ -42,7 +64,7 @@ async function updatePermissionOnRole(
// the permission is for a built in, make sure it exists
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
const builtin = cloneDeep(BUILTIN_ROLES[roleId])
const builtin = getBuiltinRoles()[roleId]
builtin._id = getDBRoleID(builtin._id)
dbRoles.push(builtin)
}
@ -65,7 +87,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
@ -84,12 +109,12 @@ async function updatePermissionOnRole(
}
exports.fetchBuiltin = function(ctx) {
ctx.body = Object.values(BUILTIN_PERMISSIONS)
ctx.body = Object.values(getBuiltinPermissions())
}
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 +123,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 +153,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) {

View file

@ -1,6 +1,6 @@
const CouchDB = require("../../db")
const {
BUILTIN_ROLES,
getBuiltinRoles,
BUILTIN_ROLE_IDS,
Role,
getRole,
@ -57,17 +57,20 @@ exports.fetch = async function(ctx) {
include_docs: true,
})
)
const roles = body.rows.map(row => row.doc)
let roles = body.rows.map(row => row.doc)
const builtinRoles = getBuiltinRoles()
// need to combine builtin with any DB record of them (for sake of permissions)
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
const builtinRole = BUILTIN_ROLES[builtinRoleId]
const builtinRole = builtinRoles[builtinRoleId]
const dbBuiltin = roles.filter(
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
)[0]
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))
}

View file

@ -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)
})
})

View file

@ -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

View file

@ -53,6 +53,10 @@ module.exports.getAction = async function(actionName) {
if (BUILTIN_ACTIONS[actionName] != null) {
return BUILTIN_ACTIONS[actionName]
}
// worker pools means that a worker may not have manifest
if (env.CLOUD && MANIFEST == null) {
MANIFEST = await module.exports.init()
}
// env setup to get async packages
if (!MANIFEST || !MANIFEST.packages || !MANIFEST.packages[actionName]) {
return null
@ -86,8 +90,10 @@ module.exports.init = async function() {
? Object.assign(MANIFEST.packages, BUILTIN_DEFINITIONS)
: BUILTIN_DEFINITIONS
} catch (err) {
console.error(err)
Sentry.captureException(err)
}
return MANIFEST
}
module.exports.DEFINITIONS = BUILTIN_DEFINITIONS

View file

@ -34,7 +34,7 @@ module.exports.init = function() {
actions.init().then(() => {
triggers.automationQueue.process(async job => {
try {
if (env.CLOUD && job.data.automation) {
if (env.CLOUD && job.data.automation && !env.SELF_HOSTED) {
job.data.automation.apiKey = await updateQuota(job.data.automation)
}
if (env.BUDIBASE_ENVIRONMENT === "PRODUCTION") {

View file

@ -1,6 +1,6 @@
const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes")
const { getRole, BUILTIN_ROLES } = require("../utilities/security/roles")
const { getRole, getBuiltinRoles } = require("../utilities/security/roles")
const { AuthTypes } = require("../constants")
const {
getAppId,
@ -20,6 +20,7 @@ module.exports = async (ctx, next) => {
// we hold it in state as a
let appId = getAppId(ctx)
const cookieAppId = ctx.cookies.get(getCookieName("currentapp"))
const builtinRoles = getBuiltinRoles()
if (appId && cookieAppId !== appId) {
setCookie(ctx, appId, "currentapp")
} else if (cookieAppId) {
@ -40,7 +41,7 @@ module.exports = async (ctx, next) => {
ctx.appId = appId
ctx.user = {
appId,
role: BUILTIN_ROLES.PUBLIC,
role: builtinRoles.PUBLIC,
}
await next()
return

View file

@ -1,4 +1,5 @@
const { flatten } = require("lodash")
const { cloneDeep } = require("lodash/fp")
const PermissionLevels = {
READ: "read",
@ -23,6 +24,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,13 +64,21 @@ function getAllowedLevels(userPermLevel) {
}
exports.BUILTIN_PERMISSION_IDS = {
PUBLIC: "public",
READ_ONLY: "read_only",
WRITE: "write",
ADMIN: "admin",
POWER: "power",
}
exports.BUILTIN_PERMISSIONS = {
const 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 +122,15 @@ exports.BUILTIN_PERMISSIONS = {
},
}
exports.getBuiltinPermissions = () => {
return cloneDeep(BUILTIN_PERMISSIONS)
}
exports.getBuiltinPermissionByID = id => {
const perms = Object.values(BUILTIN_PERMISSIONS)
return perms.find(perm => perm._id === id)
}
exports.doesHaveResourcePermission = (
permissions,
permLevel,
@ -126,7 +160,7 @@ exports.doesHaveResourcePermission = (
}
exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
const builtins = Object.values(BUILTIN_PERMISSIONS)
let permissions = flatten(
builtins
.filter(builtin => permissionIds.indexOf(builtin._id) !== -1)
@ -144,22 +178,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

View file

@ -26,7 +26,7 @@ Role.prototype.addInheritance = function(inherits) {
return this
}
exports.BUILTIN_ROLES = {
const BUILTIN_ROLES = {
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin")
.addPermission(BUILTIN_PERMISSION_IDS.ADMIN)
.addInheritance(BUILTIN_IDS.POWER),
@ -37,18 +37,22 @@ 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
),
}
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
exports.getBuiltinRoles = () => {
return cloneDeep(BUILTIN_ROLES)
}
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(BUILTIN_ROLES).map(
role => role._id
)
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(BUILTIN_ROLES).map(
role => role.name
)
@ -56,6 +60,42 @@ 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 builtins = exports.getBuiltinRoles()
const MAX = Object.values(BUILTIN_IDS).length + 1
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
return MAX
}
let role = builtins[id],
count = 0
do {
if (!role) {
break
}
role = builtins[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.
@ -72,7 +112,7 @@ exports.getRole = async (appId, roleId) => {
// but can be extended by a doc stored about them (e.g. permissions)
if (isBuiltin(roleId)) {
role = cloneDeep(
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
Object.values(BUILTIN_ROLES).find(role => role._id === roleId)
)
}
try {

View file

@ -0,0 +1,70 @@
const {
PermissionLevels,
PermissionTypes,
getBuiltinPermissionByID,
isPermissionLevelHigherThanRead,
} = require("../../utilities/security/permissions")
const {
lowerBuiltinRoleID,
getBuiltinRoles,
} = 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(getBuiltinRoles())) {
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

View file

@ -50,6 +50,9 @@ exports.Properties = {
}
exports.getAPIKey = async appId => {
if (env.SELF_HOSTED) {
return { apiKey: null }
}
return apiKeyTable.get({ primary: appId })
}
@ -63,7 +66,7 @@ exports.getAPIKey = async appId => {
*/
exports.update = async (apiKey, property, usage) => {
// don't try validate in builder
if (!env.CLOUD) {
if (!env.CLOUD || env.SELF_HOSTED) {
return
}
try {

View file

@ -181,9 +181,10 @@
"key": "subheading"
},
{
"type": "screen",
"type": "text",
"label": "Link URL",
"key": "destinationUrl"
"key": "destinationUrl",
"placeholder": "/screen"
}
]
},
@ -214,9 +215,10 @@
"key": "linkText"
},
{
"type": "screen",
"label": "Link Url",
"key": "linkUrl"
"type": "text",
"label": "Link URL",
"key": "linkUrl",
"placeholder": "/screen"
},
{
"type": "color",
@ -383,9 +385,10 @@
"key": "text"
},
{
"type": "screen",
"type": "text",
"label": "URL",
"key": "url"
"key": "url",
"placeholder": "/screen"
},
{
"type": "boolean",
@ -456,9 +459,10 @@
"key": "linkText"
},
{
"type": "screen",
"type": "text",
"label": "Link URL",
"key": "linkUrl"
"key": "linkUrl",
"placeholder": "/screen"
},
{
"type": "color",

View file

@ -35,7 +35,7 @@
"keywords": [
"svelte"
],
"version": "0.7.6",
"version": "0.7.8",
"license": "MIT",
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
"dependencies": {

View file

@ -1,7 +1,7 @@
<script>
import { getContext } from "svelte"
const { styleable } = getContext("sdk")
const { styleable, linkable } = getContext("sdk")
const component = getContext("component")
export const className = ""
@ -38,8 +38,11 @@
<h2 class="heading">{heading}</h2>
<h4 class="text">{description}</h4>
<a
use:linkable
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
href={linkUrl}>{linkText}</a>
href={linkUrl}>
{linkText}
</a>
</div>
</div>

View file

@ -1,59 +0,0 @@
<script>
import { Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "./helpers"
import { getContext } from "svelte"
const { API } = getContext("sdk")
export let schema = {}
export let linkedRows = []
export let showLabel = true
export let secondary
let linkedTable
let allRows = []
$: label = capitalise(schema.name)
$: linkedTableId = schema.tableId
$: fetchRows(linkedTableId)
$: fetchTable(linkedTableId)
async function fetchTable(id) {
if (id != null) {
linkedTable = await API.fetchTableDefinition(id)
}
}
async function fetchRows(id) {
if (id != null) {
allRows = await API.fetchTableData(id)
}
}
function getPrettyName(row) {
return row[(linkedTable && linkedTable.primaryDisplay) || "_id"]
}
</script>
{#if linkedTable != null}
{#if linkedTable.primaryDisplay == null}
{#if showLabel}
<Label extraSmall grey>{label}</Label>
{/if}
<Label small black>
Please choose a display column for the
<b>{linkedTable.name}</b>
table.
</Label>
{:else}
<Multiselect
{secondary}
bind:value={linkedRows}
label={showLabel ? label : null}
placeholder="Choose some options">
{#each allRows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{/if}
{/if}

View file

@ -18,16 +18,16 @@
let table
let fieldMap = {}
// Checks if the closest data context matches the model for this forms
// datasource, and use it as the initial form values if so
// Returns the closes data context which isn't a built in context
const getInitialValues = context => {
return context && context.tableId === datasource?.tableId ? context : {}
if (["user", "url"].includes(context.closestComponentId)) {
return {}
}
return context[`${context.closestComponentId}`] || {}
}
// Use the closest data context as the initial form values if it matches
const initialValues = getInitialValues(
$context[`${$context.closestComponentId}`]
)
// Use the closest data context as the initial form values
const initialValues = getInitialValues($context)
// Form state contains observable data about the form
const formState = writable({ values: initialValues, errors: {}, valid: true })

View file

@ -24,7 +24,7 @@
$: fetchTable(linkedTableId)
const fetchTable = async id => {
if (id != null) {
if (id) {
const result = await API.fetchTableDefinition(id)
if (!result.error) {
tableDefinition = result
@ -33,7 +33,7 @@
}
const fetchRows = async id => {
if (id != null) {
if (id) {
const rows = await API.fetchTableData(id)
options = rows && !rows.error ? rows : []
}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "0.7.6",
"version": "0.7.8",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.js",
"module": "src/index.js",

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/deployment",
"email": "hi@budibase.com",
"version": "0.7.6",
"version": "0.7.8",
"description": "Budibase Deployment Server",
"main": "src/index.js",
"repository": {