Merge branch 'master' into feat/branching-feature-flags
This commit is contained in:
commit
30c392e5a8
|
@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
|||
<br /><br />
|
||||
|
||||
### Load data or start from scratch
|
||||
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MariaDB, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680281798/ui/data_klbuna.png">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "2.31.6",
|
||||
"version": "2.31.8",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import {
|
||||
AbsTooltip,
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Button,
|
||||
Divider,
|
||||
Icon,
|
||||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
|
@ -15,6 +17,8 @@
|
|||
export let description
|
||||
export let enabled
|
||||
export let upgradeButtonClick
|
||||
|
||||
$: upgradeDisabled = !$auth.accountPortalAccess && $admin.cloud
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
|
@ -36,8 +40,9 @@
|
|||
{:else}
|
||||
<div class="buttons">
|
||||
<Button
|
||||
primary
|
||||
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||
primary={!upgradeDisabled}
|
||||
secondary={upgradeDisabled}
|
||||
disabled={upgradeDisabled}
|
||||
on:click={async () => upgradeButtonClick()}
|
||||
>
|
||||
Upgrade
|
||||
|
@ -51,6 +56,16 @@
|
|||
>
|
||||
View Plans
|
||||
</Button>
|
||||
{#if upgradeDisabled}
|
||||
<AbsTooltip
|
||||
text={"Please contact the account holder to upgrade"}
|
||||
position={"right"}
|
||||
>
|
||||
<div class="icon" on:focus>
|
||||
<Icon name="InfoOutline" size="L" disabled hoverable />
|
||||
</div>
|
||||
</AbsTooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
@ -67,7 +82,11 @@
|
|||
justify-content: flex-start;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -127,7 +127,10 @@
|
|||
name: user.firstName ? user.firstName + " " + user.lastName : "",
|
||||
userGroups,
|
||||
__selectable:
|
||||
role.value === Constants.BudibaseRoles.Owner ? false : undefined,
|
||||
role.value === Constants.BudibaseRoles.Owner ||
|
||||
$auth.user?.email === user.email
|
||||
? false
|
||||
: true,
|
||||
apps: [...new Set(Object.keys(user.roles))],
|
||||
access: role.sortOrder,
|
||||
}
|
||||
|
@ -392,7 +395,7 @@
|
|||
allowSelectRows={!readonly}
|
||||
{customRenderers}
|
||||
loading={!$fetch.loaded || !groupsLoaded}
|
||||
defaultSortColumn={"access"}
|
||||
defaultSortColumn={"__selectable"}
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
|
|
|
@ -153,7 +153,7 @@
|
|||
$: builderInteractive =
|
||||
$builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
|
||||
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
|
||||
$: interactive = !isRoot && (builderInteractive || devToolsInteractive)
|
||||
$: interactive = builderInteractive || devToolsInteractive
|
||||
$: editing = editable && selected && $builderStore.editMode
|
||||
$: draggable =
|
||||
!inDragPath &&
|
||||
|
@ -189,12 +189,6 @@
|
|||
// Scroll the selected element into view
|
||||
$: selected && scrollIntoView()
|
||||
|
||||
// When dragging and dropping, pad components to allow dropping between
|
||||
// nested layers. Only reset this when dragging stops.
|
||||
let pad = false
|
||||
$: pad = pad || (interactive && hasChildren && inDndPath)
|
||||
$: $dndIsDragging, (pad = false)
|
||||
|
||||
// Themes
|
||||
$: currentTheme = $context?.device?.theme
|
||||
$: darkMode = !currentTheme?.includes("light")
|
||||
|
@ -206,8 +200,10 @@
|
|||
}
|
||||
|
||||
// Metadata to pass into grid action to apply CSS
|
||||
const insideGrid =
|
||||
parent?._component.endsWith("/container") && parent?.layout === "grid"
|
||||
const checkGrid = x =>
|
||||
x?._component?.endsWith("/container") && x?.layout === "grid"
|
||||
$: insideGrid = checkGrid(parent)
|
||||
$: isGrid = checkGrid(instance)
|
||||
$: gridMetadata = {
|
||||
insideGrid,
|
||||
ignoresLayout: definition?.ignoresLayout === true,
|
||||
|
@ -219,6 +215,12 @@
|
|||
errored: errorState,
|
||||
}
|
||||
|
||||
// When dragging and dropping, pad components to allow dropping between
|
||||
// nested layers. Only reset this when dragging stops.
|
||||
let pad = false
|
||||
$: pad = pad || (!isGrid && interactive && hasChildren && inDndPath)
|
||||
$: $dndIsDragging, (pad = false)
|
||||
|
||||
// Update component context
|
||||
$: store.set({
|
||||
id,
|
||||
|
@ -231,12 +233,14 @@
|
|||
empty: emptyState,
|
||||
selected,
|
||||
interactive,
|
||||
isRoot,
|
||||
draggable,
|
||||
editable,
|
||||
isBlock,
|
||||
},
|
||||
empty: emptyState,
|
||||
selected,
|
||||
isRoot,
|
||||
inSelectedPath,
|
||||
name,
|
||||
editing,
|
||||
|
@ -672,6 +676,7 @@
|
|||
class:parent={hasChildren}
|
||||
class:block={isBlock}
|
||||
class:error={errorState}
|
||||
class:root={isRoot}
|
||||
data-id={id}
|
||||
data-name={name}
|
||||
data-icon={icon}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
// Get the screen definition for the current route
|
||||
$: screen = $screenStore.activeScreen
|
||||
$: screenDefinition = { ...screen?.props, addEmptyRows: true }
|
||||
$: onLoadActions.set(screen?.onLoad)
|
||||
$: runOnLoadActions($onLoadActions, params)
|
||||
|
||||
|
@ -42,10 +41,10 @@
|
|||
</script>
|
||||
|
||||
<!-- Ensure to fully remount when screen changes -->
|
||||
{#if $routeStore.routerLoaded}
|
||||
{#key screenDefinition?._id}
|
||||
{#if $routeStore.routerLoaded && screen?.props}
|
||||
{#key screen.props._id}
|
||||
<Provider key="url" data={params}>
|
||||
<Component isRoot instance={screenDefinition} />
|
||||
<Component isRoot instance={screen.props} />
|
||||
</Provider>
|
||||
{/key}
|
||||
{/if}
|
||||
|
|
|
@ -18,9 +18,11 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: var(--font-size-s);
|
||||
gap: var(--spacing-s);
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
flex: 1 1 auto;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
.placeholder :global(.spectrum-Button) {
|
||||
margin-top: var(--spacing-m);
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
import { GridRowHeight, GridColumns } from "constants"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
|
||||
export let addEmptyRows = false
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const context = getContext("context")
|
||||
|
@ -18,16 +16,12 @@
|
|||
let styles = memo({})
|
||||
|
||||
$: inBuilder = $builderStore.inBuilder
|
||||
$: requiredRows = calculateRequiredRows(
|
||||
$children,
|
||||
mobile,
|
||||
addEmptyRows && inBuilder
|
||||
)
|
||||
$: addEmptyRows = $component.isRoot && inBuilder
|
||||
$: requiredRows = calculateRequiredRows($children, mobile, addEmptyRows)
|
||||
$: requiredHeight = requiredRows * GridRowHeight
|
||||
$: availableRows = Math.floor(height / GridRowHeight)
|
||||
$: rows = Math.max(requiredRows, availableRows)
|
||||
$: mobile = $context.device.mobile
|
||||
$: empty = $component.empty
|
||||
$: colSize = width / GridColumns
|
||||
$: styles.set({
|
||||
...$component.styles,
|
||||
|
@ -40,7 +34,6 @@
|
|||
"--col-size": colSize,
|
||||
"--row-size": GridRowHeight,
|
||||
},
|
||||
empty: false,
|
||||
})
|
||||
|
||||
// Calculates the minimum number of rows required to render all child
|
||||
|
@ -145,9 +138,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Only render the slot if not empty, as we don't want the placeholder -->
|
||||
{#if !empty && mounted}
|
||||
{#if mounted}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
let left, top, height, width
|
||||
|
||||
const updatePosition = () => {
|
||||
const node =
|
||||
document.getElementsByClassName(DNDPlaceholderID)[0]?.childNodes[0]
|
||||
let node = document.getElementsByClassName(DNDPlaceholderID)[0]
|
||||
const insideGrid = node?.dataset.insideGrid === "true"
|
||||
if (!insideGrid) {
|
||||
node = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]
|
||||
}
|
||||
if (!node) {
|
||||
height = 0
|
||||
width = 0
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
newId = e.target.dataset.id
|
||||
} else {
|
||||
// Handle normal components
|
||||
const element = e.target.closest(".interactive.component")
|
||||
const element = e.target.closest(".interactive.component:not(.root)")
|
||||
newId = element?.dataset?.id
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { computed } from "../utils/computed.js"
|
||||
import { derivedMemo } from "@budibase/frontend-core"
|
||||
|
||||
const createDndStore = () => {
|
||||
const initialState = {
|
||||
|
@ -78,11 +78,11 @@ export const dndStore = createDndStore()
|
|||
// performance by deriving any state that needs to be externally observed.
|
||||
// By doing this and using primitives, we can avoid invalidating other stores
|
||||
// or components which depend on DND state unless values actually change.
|
||||
export const dndParent = computed(dndStore, x => x.drop?.parent)
|
||||
export const dndIndex = computed(dndStore, x => x.drop?.index)
|
||||
export const dndBounds = computed(dndStore, x => x.source?.bounds)
|
||||
export const dndIsDragging = computed(dndStore, x => !!x.source)
|
||||
export const dndIsNewComponent = computed(
|
||||
export const dndParent = derivedMemo(dndStore, x => x.drop?.parent)
|
||||
export const dndIndex = derivedMemo(dndStore, x => x.drop?.index)
|
||||
export const dndBounds = derivedMemo(dndStore, x => x.source?.bounds)
|
||||
export const dndIsDragging = derivedMemo(dndStore, x => !!x.source)
|
||||
export const dndIsNewComponent = derivedMemo(
|
||||
dndStore,
|
||||
x => x.source?.newComponentType != null
|
||||
)
|
||||
|
|
|
@ -92,6 +92,8 @@ const createScreenStore = () => {
|
|||
width: `${$dndBounds?.width || 400}px`,
|
||||
height: `${$dndBounds?.height || 200}px`,
|
||||
opacity: 0,
|
||||
"--default-width": $dndBounds?.width || 400,
|
||||
"--default-height": $dndBounds?.height || 200,
|
||||
},
|
||||
},
|
||||
static: true,
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
/**
|
||||
* Extension of Svelte's built in "derived" stores, which the addition of deep
|
||||
* comparison of non-primitives. Falls back to using shallow comparison for
|
||||
* primitive types to avoid performance penalties.
|
||||
* Useful for instances where a deep comparison is cheaper than an additional
|
||||
* store invalidation.
|
||||
* @param store the store to observer
|
||||
* @param deriveValue the derivation function
|
||||
* @returns {Writable<*>} a derived svelte store containing just the derived value
|
||||
*/
|
||||
export const computed = (store, deriveValue) => {
|
||||
const initialValue = deriveValue(store)
|
||||
const computedStore = writable(initialValue)
|
||||
let lastKey = getKey(initialValue)
|
||||
|
||||
store.subscribe(state => {
|
||||
const value = deriveValue(state)
|
||||
const key = getKey(value)
|
||||
if (key !== lastKey) {
|
||||
lastKey = key
|
||||
computedStore.set(value)
|
||||
}
|
||||
})
|
||||
|
||||
return computedStore
|
||||
}
|
||||
|
||||
// Helper function to serialise any value into a primitive which can be cheaply
|
||||
// and shallowly compared
|
||||
const getKey = value => {
|
||||
if (value == null || typeof value !== "object") {
|
||||
return value
|
||||
} else {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
}
|
|
@ -92,8 +92,12 @@ export const gridLayout = (node, metadata) => {
|
|||
}
|
||||
|
||||
// Determine default width and height of component
|
||||
let width = errored ? 500 : definition?.size?.width || 200
|
||||
let height = errored ? 60 : definition?.size?.height || 200
|
||||
let width = styles["--default-width"] ?? definition?.size?.width ?? 200
|
||||
let height = styles["--default-height"] ?? definition?.size?.height ?? 200
|
||||
if (errored) {
|
||||
width = 500
|
||||
height = 60
|
||||
}
|
||||
width += 2 * GridSpacing
|
||||
height += 2 * GridSpacing
|
||||
let vars = {
|
||||
|
|
|
@ -93,7 +93,7 @@ export const styleable = (node, styles = {}) => {
|
|||
node.addEventListener("mouseout", applyNormalStyles)
|
||||
|
||||
// Add builder preview click listener
|
||||
if (newStyles.interactive) {
|
||||
if (newStyles.interactive && !newStyles.isRoot) {
|
||||
node.addEventListener("click", selectComponent, false)
|
||||
node.addEventListener("dblclick", editComponent, false)
|
||||
}
|
||||
|
|
|
@ -76,7 +76,9 @@ export const ExtendedBudibaseRoleOptions = [
|
|||
value: BudibaseRoles.Owner,
|
||||
sortOrder: 0,
|
||||
},
|
||||
].concat(BudibaseRoleOptions)
|
||||
]
|
||||
.concat(BudibaseRoleOptions)
|
||||
.concat(BudibaseRoleOptionsOld)
|
||||
|
||||
export const PlanType = {
|
||||
FREE: "free",
|
||||
|
|
|
@ -5,11 +5,10 @@ import {
|
|||
CreateRowActionRequest,
|
||||
DocumentType,
|
||||
PermissionLevel,
|
||||
Row,
|
||||
RowActionResponse,
|
||||
} from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||
import { Expectations } from "../../../tests/utilities/api/base"
|
||||
import { roles } from "@budibase/backend-core"
|
||||
import { automations } from "@budibase/pro"
|
||||
|
@ -651,13 +650,27 @@ describe("/rowsActions", () => {
|
|||
})
|
||||
|
||||
describe("trigger", () => {
|
||||
let row: Row
|
||||
let viewId: string
|
||||
let rowId: string
|
||||
let rowAction: RowActionResponse
|
||||
|
||||
beforeEach(async () => {
|
||||
row = await config.api.row.save(tableId, {})
|
||||
const row = await config.api.row.save(tableId, {})
|
||||
rowId = row._id!
|
||||
rowAction = await createRowAction(tableId, createRowActionRequest())
|
||||
|
||||
viewId = (
|
||||
await config.api.viewV2.create(
|
||||
setup.structures.viewV2.createRequest(tableId)
|
||||
)
|
||||
).id
|
||||
|
||||
await config.api.rowAction.setViewPermission(
|
||||
tableId,
|
||||
viewId,
|
||||
rowAction.id
|
||||
)
|
||||
|
||||
await config.publish()
|
||||
tk.travel(Date.now() + 100)
|
||||
})
|
||||
|
@ -673,9 +686,7 @@ describe("/rowsActions", () => {
|
|||
|
||||
it("can trigger an automation given valid data", async () => {
|
||||
expect(await getAutomationLogs()).toBeEmpty()
|
||||
await config.api.rowAction.trigger(tableId, rowAction.id, {
|
||||
rowId: row._id!,
|
||||
})
|
||||
await config.api.rowAction.trigger(viewId, rowAction.id, { rowId })
|
||||
|
||||
const automationLogs = await getAutomationLogs()
|
||||
expect(automationLogs).toEqual([
|
||||
|
@ -687,8 +698,11 @@ describe("/rowsActions", () => {
|
|||
inputs: null,
|
||||
outputs: {
|
||||
fields: {},
|
||||
row: await config.api.row.get(tableId, row._id!),
|
||||
table: await config.api.table.get(tableId),
|
||||
row: await config.api.row.get(tableId, rowId),
|
||||
table: {
|
||||
...(await config.api.table.get(tableId)),
|
||||
views: expect.anything(),
|
||||
},
|
||||
automation: expect.objectContaining({
|
||||
_id: rowAction.automationId,
|
||||
}),
|
||||
|
@ -709,9 +723,7 @@ describe("/rowsActions", () => {
|
|||
await config.api.rowAction.trigger(
|
||||
viewId,
|
||||
rowAction.id,
|
||||
{
|
||||
rowId: row._id!,
|
||||
},
|
||||
{ rowId },
|
||||
{
|
||||
status: 403,
|
||||
body: {
|
||||
|
@ -738,10 +750,9 @@ describe("/rowsActions", () => {
|
|||
)
|
||||
|
||||
await config.publish()
|
||||
|
||||
expect(await getAutomationLogs()).toBeEmpty()
|
||||
await config.api.rowAction.trigger(viewId, rowAction.id, {
|
||||
rowId: row._id!,
|
||||
})
|
||||
await config.api.rowAction.trigger(viewId, rowAction.id, { rowId })
|
||||
|
||||
const automationLogs = await getAutomationLogs()
|
||||
expect(automationLogs).toEqual([
|
||||
|
@ -750,5 +761,170 @@ describe("/rowsActions", () => {
|
|||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe("role permission checks", () => {
|
||||
beforeAll(() => {
|
||||
mocks.licenses.useViewPermissions()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mocks.licenses.useCloudFree()
|
||||
})
|
||||
|
||||
function createUser(role: string) {
|
||||
return config.createUser({
|
||||
admin: { global: false },
|
||||
builder: {},
|
||||
roles: { [config.getProdAppId()]: role },
|
||||
})
|
||||
}
|
||||
|
||||
const allowedRoleConfig = (() => {
|
||||
function getRolesLowerThan(role: string) {
|
||||
const result = Object.values(roles.BUILTIN_ROLE_IDS).filter(
|
||||
r => r !== role && roles.lowerBuiltinRoleID(r, role) === r
|
||||
)
|
||||
return result
|
||||
}
|
||||
return Object.values(roles.BUILTIN_ROLE_IDS).flatMap(r =>
|
||||
[r, ...getRolesLowerThan(r)].map(p => [r, p])
|
||||
)
|
||||
})()
|
||||
|
||||
const disallowedRoleConfig = (() => {
|
||||
function getRolesHigherThan(role: string) {
|
||||
const result = Object.values(roles.BUILTIN_ROLE_IDS).filter(
|
||||
r => r !== role && roles.lowerBuiltinRoleID(r, role) === role
|
||||
)
|
||||
return result
|
||||
}
|
||||
return Object.values(roles.BUILTIN_ROLE_IDS).flatMap(r =>
|
||||
getRolesHigherThan(r).map(p => [r, p])
|
||||
)
|
||||
})()
|
||||
|
||||
describe.each([
|
||||
[
|
||||
"view (with implicit views)",
|
||||
async () => {
|
||||
const viewId = (
|
||||
await config.api.viewV2.create(
|
||||
setup.structures.viewV2.createRequest(tableId)
|
||||
)
|
||||
).id
|
||||
|
||||
await config.api.rowAction.setViewPermission(
|
||||
tableId,
|
||||
viewId,
|
||||
rowAction.id
|
||||
)
|
||||
return { permissionResource: viewId, triggerResouce: viewId }
|
||||
},
|
||||
],
|
||||
[
|
||||
"view (without implicit views)",
|
||||
async () => {
|
||||
const viewId = (
|
||||
await config.api.viewV2.create(
|
||||
setup.structures.viewV2.createRequest(tableId)
|
||||
)
|
||||
).id
|
||||
|
||||
await config.api.rowAction.setViewPermission(
|
||||
tableId,
|
||||
viewId,
|
||||
rowAction.id
|
||||
)
|
||||
return { permissionResource: tableId, triggerResouce: viewId }
|
||||
},
|
||||
],
|
||||
])("checks for %s", (_, getResources) => {
|
||||
it.each(allowedRoleConfig)(
|
||||
"allows triggering if the user has read permission (user %s, table %s)",
|
||||
async (userRole, resourcePermission) => {
|
||||
const { permissionResource, triggerResouce } = await getResources()
|
||||
|
||||
await config.api.permission.add({
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: permissionResource,
|
||||
roleId: resourcePermission,
|
||||
})
|
||||
|
||||
const normalUser = await createUser(userRole)
|
||||
|
||||
await config.withUser(normalUser, async () => {
|
||||
await config.publish()
|
||||
await config.api.rowAction.trigger(
|
||||
triggerResouce,
|
||||
rowAction.id,
|
||||
{ rowId },
|
||||
{ status: 200 }
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each(disallowedRoleConfig)(
|
||||
"rejects if the user does not have table read permission (user %s, table %s)",
|
||||
async (userRole, resourcePermission) => {
|
||||
const { permissionResource, triggerResouce } = await getResources()
|
||||
await config.api.permission.add({
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: permissionResource,
|
||||
roleId: resourcePermission,
|
||||
})
|
||||
|
||||
const normalUser = await createUser(userRole)
|
||||
|
||||
await config.withUser(normalUser, async () => {
|
||||
await config.publish()
|
||||
await config.api.rowAction.trigger(
|
||||
triggerResouce,
|
||||
rowAction.id,
|
||||
{ rowId },
|
||||
{
|
||||
status: 403,
|
||||
body: { message: "User does not have permission" },
|
||||
}
|
||||
)
|
||||
|
||||
const automationLogs = await getAutomationLogs()
|
||||
expect(automationLogs).toBeEmpty()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it.each(allowedRoleConfig)(
|
||||
"does not allow running row actions for tables by default even",
|
||||
async (userRole, resourcePermission) => {
|
||||
await config.api.permission.add({
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: tableId,
|
||||
roleId: resourcePermission,
|
||||
})
|
||||
|
||||
const normalUser = await createUser(userRole)
|
||||
|
||||
await config.withUser(normalUser, async () => {
|
||||
await config.publish()
|
||||
await config.api.rowAction.trigger(
|
||||
tableId,
|
||||
rowAction.id,
|
||||
{ rowId },
|
||||
{
|
||||
status: 403,
|
||||
body: {
|
||||
message: `Row action '${rowAction.id}' is not enabled for table '${tableId}'`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const automationLogs = await getAutomationLogs()
|
||||
expect(automationLogs).toBeEmpty()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -88,7 +88,7 @@ const authorized =
|
|||
opts = { schema: false },
|
||||
resourcePath?: string
|
||||
) =>
|
||||
async (ctx: any, next: any) => {
|
||||
async (ctx: UserCtx, next: any) => {
|
||||
// webhooks don't need authentication, each webhook unique
|
||||
// also internal requests (between services) don't need authorized
|
||||
if (isWebhookEndpoint(ctx) || ctx.internal) {
|
||||
|
|
|
@ -1,40 +1,52 @@
|
|||
import { Next } from "koa"
|
||||
import { Ctx } from "@budibase/types"
|
||||
import { paramSubResource } from "./resourceId"
|
||||
import { PermissionLevel, PermissionType, UserCtx } from "@budibase/types"
|
||||
import { docIds } from "@budibase/backend-core"
|
||||
import * as utils from "../db/utils"
|
||||
import sdk from "../sdk"
|
||||
import { authorizedResource } from "./authorized"
|
||||
|
||||
export function triggerRowActionAuthorised(
|
||||
sourcePath: string,
|
||||
actionPath: string
|
||||
) {
|
||||
return async (ctx: Ctx, next: Next) => {
|
||||
// Reusing the existing middleware to extract the value
|
||||
paramSubResource(sourcePath, actionPath)(ctx, () => {})
|
||||
const { resourceId: sourceId, subResourceId: rowActionId } = ctx
|
||||
return async (ctx: UserCtx, next: Next) => {
|
||||
async function getResourceIds() {
|
||||
const sourceId: string = ctx.params[sourcePath]
|
||||
const rowActionId: string = ctx.params[actionPath]
|
||||
|
||||
const isTableId = docIds.isTableId(sourceId)
|
||||
const isViewId = utils.isViewID(sourceId)
|
||||
if (!isTableId && !isViewId) {
|
||||
ctx.throw(400, `'${sourceId}' is not a valid source id`)
|
||||
const isTableId = docIds.isTableId(sourceId)
|
||||
const isViewId = utils.isViewID(sourceId)
|
||||
if (!isTableId && !isViewId) {
|
||||
ctx.throw(400, `'${sourceId}' is not a valid source id`)
|
||||
}
|
||||
|
||||
const tableId = isTableId
|
||||
? sourceId
|
||||
: utils.extractViewInfoFromID(sourceId).tableId
|
||||
const viewId = isTableId ? undefined : sourceId
|
||||
return { tableId, viewId, rowActionId }
|
||||
}
|
||||
|
||||
const tableId = isTableId
|
||||
? sourceId
|
||||
: utils.extractViewInfoFromID(sourceId).tableId
|
||||
const { tableId, viewId, rowActionId } = await getResourceIds()
|
||||
|
||||
// Check if the user has permissions to the table/view
|
||||
await authorizedResource(
|
||||
!viewId ? PermissionType.TABLE : PermissionType.VIEW,
|
||||
PermissionLevel.READ,
|
||||
sourcePath
|
||||
)(ctx, () => {})
|
||||
|
||||
// Check is the row action can run for the given view/table
|
||||
const rowAction = await sdk.rowActions.get(tableId, rowActionId)
|
||||
|
||||
if (isTableId && !rowAction.permissions.table.runAllowed) {
|
||||
if (!viewId && !rowAction.permissions.table.runAllowed) {
|
||||
ctx.throw(
|
||||
403,
|
||||
`Row action '${rowActionId}' is not enabled for table '${sourceId}'`
|
||||
`Row action '${rowActionId}' is not enabled for table '${tableId}'`
|
||||
)
|
||||
} else if (isViewId && !rowAction.permissions.views[sourceId]?.runAllowed) {
|
||||
} else if (viewId && !rowAction.permissions.views[viewId]?.runAllowed) {
|
||||
ctx.throw(
|
||||
403,
|
||||
`Row action '${rowActionId}' is not enabled for view '${sourceId}'`
|
||||
`Row action '${rowActionId}' is not enabled for view '${viewId}'`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ export async function create(tableId: string, rowAction: { name: string }) {
|
|||
name: action.name,
|
||||
automationId: automation._id!,
|
||||
permissions: {
|
||||
table: { runAllowed: true },
|
||||
table: { runAllowed: false },
|
||||
views: {},
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue