diff --git a/README.md b/README.md
index 64492b97e4..26ad9f80c2 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
### 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).
diff --git a/lerna.json b/lerna.json
index 9b2404edfd..0292b863e8 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "2.31.6",
+ "version": "2.31.8",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/packages/builder/src/pages/builder/portal/_components/LockedFeature.svelte b/packages/builder/src/pages/builder/portal/_components/LockedFeature.svelte
index e6f4075e2e..1df724099b 100644
--- a/packages/builder/src/pages/builder/portal/_components/LockedFeature.svelte
+++ b/packages/builder/src/pages/builder/portal/_components/LockedFeature.svelte
@@ -1,10 +1,12 @@
@@ -36,8 +40,9 @@
{:else}
{/if}
@@ -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;
diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte
index b91b2129e6..a1d5496ff6 100644
--- a/packages/builder/src/pages/builder/portal/users/users/index.svelte
+++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte
@@ -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"}
/>
{/if}
-
-
- {#if !empty && mounted}
+ {#if mounted}
{/if}
diff --git a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte
index 0ad4280e07..61cecc885b 100644
--- a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte
+++ b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte
@@ -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
diff --git a/packages/client/src/components/preview/HoverIndicator.svelte b/packages/client/src/components/preview/HoverIndicator.svelte
index d204d77f49..f1c151ce16 100644
--- a/packages/client/src/components/preview/HoverIndicator.svelte
+++ b/packages/client/src/components/preview/HoverIndicator.svelte
@@ -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
}
diff --git a/packages/client/src/stores/dnd.js b/packages/client/src/stores/dnd.js
index 1bdd510eb7..43d4eeeed8 100644
--- a/packages/client/src/stores/dnd.js
+++ b/packages/client/src/stores/dnd.js
@@ -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
)
diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js
index 3c5ece0a6c..bc87216660 100644
--- a/packages/client/src/stores/screens.js
+++ b/packages/client/src/stores/screens.js
@@ -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,
diff --git a/packages/client/src/utils/computed.js b/packages/client/src/utils/computed.js
deleted file mode 100644
index aa89e7ad1b..0000000000
--- a/packages/client/src/utils/computed.js
+++ /dev/null
@@ -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)
- }
-}
diff --git a/packages/client/src/utils/grid.js b/packages/client/src/utils/grid.js
index 1727b904ca..142a7ed55a 100644
--- a/packages/client/src/utils/grid.js
+++ b/packages/client/src/utils/grid.js
@@ -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 = {
diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js
index 0f484a9ab9..884420a1fd 100644
--- a/packages/client/src/utils/styleable.js
+++ b/packages/client/src/utils/styleable.js
@@ -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)
}
diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js
index 1f82610408..d2f25b351a 100644
--- a/packages/frontend-core/src/constants.js
+++ b/packages/frontend-core/src/constants.js
@@ -76,7 +76,9 @@ export const ExtendedBudibaseRoleOptions = [
value: BudibaseRoles.Owner,
sortOrder: 0,
},
-].concat(BudibaseRoleOptions)
+]
+ .concat(BudibaseRoleOptions)
+ .concat(BudibaseRoleOptionsOld)
export const PlanType = {
FREE: "free",
diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index d43f5075d0..3f4447c50d 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -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()
+ })
+ }
+ )
+ })
})
})
diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts
index b23a9846b7..3bead7f80d 100644
--- a/packages/server/src/middleware/authorized.ts
+++ b/packages/server/src/middleware/authorized.ts
@@ -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) {
diff --git a/packages/server/src/middleware/triggerRowActionAuthorised.ts b/packages/server/src/middleware/triggerRowActionAuthorised.ts
index be5c6a97e1..17f22b7000 100644
--- a/packages/server/src/middleware/triggerRowActionAuthorised.ts
+++ b/packages/server/src/middleware/triggerRowActionAuthorised.ts
@@ -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}'`
)
}
diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts
index e963a4317b..9a7d402df0 100644
--- a/packages/server/src/sdk/app/rowActions.ts
+++ b/packages/server/src/sdk/app/rowActions.ts
@@ -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: {},
},
}