Merge pull request #14218 from Budibase/BUDI-8430/prevent-edits-and-deletions

Prevent edits and deletions for row action automations
This commit is contained in:
Adria Navarro 2024-07-23 12:34:45 +02:00 committed by GitHub
commit 421e42d8fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 142 additions and 62 deletions

View File

@ -6,6 +6,7 @@
contextMenuStore,
} from "stores/builder"
import { notifications, Icon } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
import NavItem from "components/common/NavItem.svelte"
@ -35,45 +36,53 @@
}
const getContextMenuItems = () => {
return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: confirmDeleteDialog.show,
const isRowAction = sdk.automations.isRowAction(automation)
const result = []
if (!isRowAction) {
result.push(
...[
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: confirmDeleteDialog.show,
},
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: updateAutomationDialog.show,
},
{
icon: "Duplicate",
name: "Duplicate",
keyBind: null,
visible: true,
disabled: automation.definition.trigger.name === "Webhook",
callback: duplicateAutomation,
},
]
)
}
result.push({
icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
name: automation.disabled ? "Activate" : "Pause",
keyBind: null,
visible: true,
disabled: false,
callback: () => {
automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)
},
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: updateAutomationDialog.show,
},
{
icon: "Duplicate",
name: "Duplicate",
keyBind: null,
visible: true,
disabled: automation.definition.trigger.name === "Webhook",
callback: duplicateAutomation,
},
{
icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
name: automation.disabled ? "Activate" : "Pause",
keyBind: null,
visible: true,
disabled: false,
callback: () => {
automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)
},
},
]
})
return result
}
const openContextMenu = e => {
@ -89,7 +98,7 @@
on:contextmenu={openContextMenu}
{icon}
iconColor={"var(--spectrum-global-color-gray-900)"}
text={automation.name}
text={automation.displayName}
selected={automation._id === $selectedAutomation?._id}
hovering={automation._id === $contextMenuStore.id}
on:click={() => automationStore.actions.select(automation._id)}

View File

@ -19,13 +19,13 @@
})
.map(automation => ({
...automation,
name:
displayName:
$automationStore.automationDisplayData[automation._id].displayName ||
automation.name,
}))
.sort((a, b) => {
const lowerA = a.name.toLowerCase()
const lowerB = b.name.toLowerCase()
const lowerA = a.displayName.toLowerCase()
const lowerB = b.displayName.toLowerCase()
return lowerA > lowerB ? 1 : -1
})

View File

@ -104,19 +104,8 @@ const automationActions = store => ({
},
save: async automation => {
const response = await API.updateAutomation(automation)
store.update(state => {
const updatedAutomation = response.automation
const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id
)
if (existingIdx !== -1) {
state.automations.splice(existingIdx, 1, updatedAutomation)
return state
} else {
state.automations = [...state.automations, updatedAutomation]
}
return state
})
await store.actions.fetch()
return response.automation
},
delete: async automation => {

View File

@ -1,4 +1,5 @@
import * as triggers from "../../automations/triggers"
import { sdk as coreSdk } from "@budibase/shared-core"
import { DocumentType } from "../../db/utils"
import { updateTestHistory, removeDeprecated } from "../../automations/utils"
import { setTestFlag, clearTestFlag } from "../../utilities/redis"
@ -94,6 +95,11 @@ export async function find(ctx: UserCtx) {
export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
const automationId = ctx.params.id
const automation = await sdk.automations.get(ctx.params.id)
if (coreSdk.automations.isRowAction(automation)) {
ctx.throw("Row actions automations cannot be deleted", 422)
}
ctx.body = await sdk.automations.remove(automationId, ctx.params.rev)
builderSocket?.emitAutomationDeletion(ctx, automationId)
}

View File

@ -425,6 +425,22 @@ describe("/automations", () => {
expect(events.automation.deleted).toHaveBeenCalledTimes(1)
})
it("cannot delete a row action automation", async () => {
const automation = await config.createAutomation(
setup.structures.rowActionAutomation()
)
await request
.delete(`/api/automations/${automation._id}/${automation._rev}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(422, {
message: "Row actions automations cannot be deleted",
status: 422,
})
expect(events.automation.deleted).not.toHaveBeenCalled()
})
it("should apply authorization to endpoint", async () => {
const automation = await config.createAutomation()
await checkBuilderEndpoint({

View File

@ -1,4 +1,5 @@
import { Automation, Webhook, WebhookActionType } from "@budibase/types"
import { sdk } from "@budibase/shared-core"
import { generateAutomationID, getAutomationParams } from "../../../db/utils"
import { deleteEntityMetadata } from "../../../utilities"
import { MetadataTypes } from "../../../constants"
@ -117,7 +118,6 @@ export async function create(automation: Automation) {
export async function update(automation: Automation) {
automation = { ...automation }
if (!automation._id || !automation._rev) {
throw new HTTPError("_id or _rev fields missing", 400)
}
@ -281,4 +281,11 @@ function guardInvalidUpdatesAndThrow(
}
})
}
if (
sdk.automations.isRowAction(automation) &&
automation.name !== oldAutomation.name
) {
throw new Error("Row actions cannot be renamed")
}
}

View File

@ -1,5 +1,6 @@
import { sample } from "lodash/fp"
import { Automation } from "@budibase/types"
import { Automation, AutomationTriggerStepId } from "@budibase/types"
import { generator } from "@budibase/backend-core/tests"
import automationSdk from "../"
import { structures } from "../../../../api/routes/tests/utilities"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
@ -12,6 +13,38 @@ describe("automation sdk", () => {
})
describe("update", () => {
it("can rename existing automations", async () => {
await config.doInContext(config.getAppId(), async () => {
const automation = structures.newAutomation()
const response = await automationSdk.create(automation)
const newName = generator.guid()
const update = { ...response, name: newName }
const result = await automationSdk.update(update)
expect(result.name).toEqual(newName)
})
})
it("cannot rename row action automations", async () => {
await config.doInContext(config.getAppId(), async () => {
const automation = structures.newAutomation({
trigger: {
...structures.automationTrigger(),
stepId: AutomationTriggerStepId.ROW_ACTION,
},
})
const response = await automationSdk.create(automation)
const newName = generator.guid()
const update = { ...response, name: newName }
await expect(automationSdk.update(update)).rejects.toThrow(
"Row actions cannot be renamed"
)
})
})
it.each([
["trigger", (a: Automation) => a.definition.trigger],
["step", (a: Automation) => a.definition.steps[0]],

View File

@ -2,9 +2,9 @@ import {
Automation,
AutomationActionStepId,
AutomationBuilderData,
AutomationTriggerStepId,
TableRowActions,
} from "@budibase/types"
import { sdk as coreSdk } from "@budibase/shared-core"
export function checkForCollectStep(automation: Automation) {
return automation.definition.steps.some(
@ -39,14 +39,13 @@ export async function getBuilderData(
const result: Record<string, AutomationBuilderData> = {}
for (const automation of automations) {
const { trigger } = automation.definition
const isRowAction = trigger.stepId === AutomationTriggerStepId.ROW_ACTION
const isRowAction = coreSdk.automations.isRowAction(automation)
if (!isRowAction) {
result[automation._id!] = { displayName: automation.name }
continue
}
const { tableId, rowActionId } = trigger.inputs
const { tableId, rowActionId } = automation.definition.trigger.inputs
const tableName = await getTableName(tableId)

View File

@ -158,7 +158,10 @@ export function automationTrigger(
}
}
export function newAutomation({ steps, trigger }: any = {}) {
export function newAutomation({
steps,
trigger,
}: { steps?: AutomationStep[]; trigger?: AutomationTrigger } = {}) {
const automation = basicAutomation()
if (trigger) {
@ -176,6 +179,16 @@ export function newAutomation({ steps, trigger }: any = {}) {
return automation
}
export function rowActionAutomation() {
const automation = newAutomation({
trigger: {
...automationTrigger(),
stepId: AutomationTriggerStepId.ROW_ACTION,
},
})
return automation
}
export function basicAutomation(appId?: string): Automation {
return {
name: "My Automation",

View File

@ -0,0 +1,7 @@
import { Automation, AutomationTriggerStepId } from "@budibase/types"
export function isRowAction(automation: Automation) {
const result =
automation.definition.trigger.stepId === AutomationTriggerStepId.ROW_ACTION
return result
}

View File

@ -1,2 +1,3 @@
export * as applications from "./applications"
export * as automations from "./automations"
export * as users from "./users"