From baa5a86ebb4d7983e0c31386743d1cfa0446079e Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 14:01:46 +0200
Subject: [PATCH 01/10] Rename allowedViews to allowedSources

---
 packages/server/src/api/controllers/rowAction/crud.ts  | 10 +++++-----
 packages/server/src/api/routes/tests/rowAction.spec.ts |  6 +++---
 packages/types/src/api/web/app/rowAction.ts            |  2 +-
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts
index c9b7756987..78a940c18f 100644
--- a/packages/server/src/api/controllers/rowAction/crud.ts
+++ b/packages/server/src/api/controllers/rowAction/crud.ts
@@ -36,7 +36,7 @@ export async function find(ctx: Ctx<void, RowActionsResponse>) {
           tableId: table._id!,
           name: action.name,
           automationId: action.automationId,
-          allowedViews: flattenAllowedViews(action.permissions.views),
+          allowedSources: flattenAllowedViews(action.permissions.views),
         },
       }),
       {}
@@ -59,7 +59,7 @@ export async function create(
     id: createdAction.id,
     name: createdAction.name,
     automationId: createdAction.automationId,
-    allowedViews: undefined,
+    allowedSources: undefined,
   }
   ctx.status = 201
 }
@@ -79,7 +79,7 @@ export async function update(
     id: action.id,
     name: action.name,
     automationId: action.automationId,
-    allowedViews: undefined,
+    allowedSources: undefined,
   }
 }
 
@@ -105,7 +105,7 @@ export async function setViewPermission(ctx: Ctx<void, RowActionResponse>) {
     id: action.id,
     name: action.name,
     automationId: action.automationId,
-    allowedViews: flattenAllowedViews(action.permissions.views),
+    allowedSources: flattenAllowedViews(action.permissions.views),
   }
 }
 
@@ -124,7 +124,7 @@ export async function unsetViewPermission(ctx: Ctx<void, RowActionResponse>) {
     id: action.id,
     name: action.name,
     automationId: action.automationId,
-    allowedViews: flattenAllowedViews(action.permissions.views),
+    allowedSources: flattenAllowedViews(action.permissions.views),
   }
 }
 
diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index 4fe248984a..7c1d88cc65 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -576,10 +576,10 @@ describe("/rowsActions", () => {
       )
 
       const expectedAction1 = expect.objectContaining({
-        allowedViews: [viewId1, viewId2],
+        allowedSources: [viewId1, viewId2],
       })
       const expectedAction2 = expect.objectContaining({
-        allowedViews: [viewId1],
+        allowedSources: [viewId1],
       })
 
       const expectedActions = expect.objectContaining({
@@ -601,7 +601,7 @@ describe("/rowsActions", () => {
       )
 
       const expectedAction = expect.objectContaining({
-        allowedViews: [viewId2],
+        allowedSources: [viewId2],
       })
       expect(actionResult).toEqual(expectedAction)
       expect(
diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts
index 37cfaf0fbd..0061be275e 100644
--- a/packages/types/src/api/web/app/rowAction.ts
+++ b/packages/types/src/api/web/app/rowAction.ts
@@ -8,7 +8,7 @@ export interface RowActionResponse extends RowActionData {
   id: string
   tableId: string
   automationId: string
-  allowedViews: string[] | undefined
+  allowedSources: string[] | undefined
 }
 
 export interface RowActionsResponse {

From 8764a8c6e8f8c51074873e1d97ab12445029f1aa Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 14:02:30 +0200
Subject: [PATCH 02/10] Return allowed run from table

---
 .../src/api/controllers/rowAction/crud.ts     | 51 ++++++++++++-------
 packages/types/src/documents/app/rowAction.ts | 10 ++--
 2 files changed, 38 insertions(+), 23 deletions(-)

diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts
index 78a940c18f..579f7e5f78 100644
--- a/packages/server/src/api/controllers/rowAction/crud.ts
+++ b/packages/server/src/api/controllers/rowAction/crud.ts
@@ -1,6 +1,7 @@
 import {
   CreateRowActionRequest,
   Ctx,
+  RowActionPermissions,
   RowActionResponse,
   RowActionsResponse,
   UpdateRowActionRequest,
@@ -18,25 +19,26 @@ async function getTable(ctx: Ctx) {
 
 export async function find(ctx: Ctx<void, RowActionsResponse>) {
   const table = await getTable(ctx)
+  const tableId = table._id!
 
-  if (!(await sdk.rowActions.docExists(table._id!))) {
+  if (!(await sdk.rowActions.docExists(tableId))) {
     ctx.body = {
       actions: {},
     }
     return
   }
 
-  const { actions } = await sdk.rowActions.getAll(table._id!)
+  const { actions } = await sdk.rowActions.getAll(tableId)
   const result: RowActionsResponse = {
     actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
       (acc, [key, action]) => ({
         ...acc,
         [key]: {
           id: key,
-          tableId: table._id!,
+          tableId,
           name: action.name,
           automationId: action.automationId,
-          allowedSources: flattenAllowedViews(action.permissions.views),
+          allowedSources: flattenAllowedSources(tableId, action.permissions),
         },
       }),
       {}
@@ -49,13 +51,14 @@ export async function create(
   ctx: Ctx<CreateRowActionRequest, RowActionResponse>
 ) {
   const table = await getTable(ctx)
+  const tableId = table._id!
 
-  const createdAction = await sdk.rowActions.create(table._id!, {
+  const createdAction = await sdk.rowActions.create(tableId, {
     name: ctx.request.body.name,
   })
 
   ctx.body = {
-    tableId: table._id!,
+    tableId,
     id: createdAction.id,
     name: createdAction.name,
     automationId: createdAction.automationId,
@@ -68,14 +71,15 @@ export async function update(
   ctx: Ctx<UpdateRowActionRequest, RowActionResponse>
 ) {
   const table = await getTable(ctx)
+  const tableId = table._id!
   const { actionId } = ctx.params
 
-  const action = await sdk.rowActions.update(table._id!, actionId, {
+  const action = await sdk.rowActions.update(tableId, actionId, {
     name: ctx.request.body.name,
   })
 
   ctx.body = {
-    tableId: table._id!,
+    tableId,
     id: action.id,
     name: action.name,
     automationId: action.automationId,
@@ -93,47 +97,56 @@ export async function remove(ctx: Ctx<void, void>) {
 
 export async function setViewPermission(ctx: Ctx<void, RowActionResponse>) {
   const table = await getTable(ctx)
+  const tableId = table._id!
   const { actionId, viewId } = ctx.params
 
   const action = await sdk.rowActions.setViewPermission(
-    table._id!,
+    tableId,
     actionId,
     viewId
   )
   ctx.body = {
-    tableId: table._id!,
+    tableId,
     id: action.id,
     name: action.name,
     automationId: action.automationId,
-    allowedSources: flattenAllowedViews(action.permissions.views),
+    allowedSources: flattenAllowedSources(tableId, action.permissions),
   }
 }
 
 export async function unsetViewPermission(ctx: Ctx<void, RowActionResponse>) {
   const table = await getTable(ctx)
+  const tableId = table._id!
   const { actionId, viewId } = ctx.params
 
   const action = await sdk.rowActions.unsetViewPermission(
-    table._id!,
+    tableId,
     actionId,
     viewId
   )
 
   ctx.body = {
-    tableId: table._id!,
+    tableId,
     id: action.id,
     name: action.name,
     automationId: action.automationId,
-    allowedSources: flattenAllowedViews(action.permissions.views),
+    allowedSources: flattenAllowedSources(tableId, action.permissions),
   }
 }
 
-function flattenAllowedViews(
-  permissions: Record<string, { runAllowed: boolean }>
+function flattenAllowedSources(
+  tableId: string,
+  permissions: RowActionPermissions
 ) {
-  const allowedPermissions = Object.entries(permissions || {})
-    .filter(([_, p]) => p.runAllowed)
-    .map(([viewId]) => viewId)
+  const allowedPermissions = []
+  if (permissions.table.runAllowed) {
+    allowedPermissions.push(tableId)
+  }
+  allowedPermissions.push(
+    ...Object.keys(permissions.views || {}).filter(
+      viewId => permissions.views[viewId].runAllowed
+    )
+  )
   if (!allowedPermissions.length) {
     return undefined
   }
diff --git a/packages/types/src/documents/app/rowAction.ts b/packages/types/src/documents/app/rowAction.ts
index fc9a25c2e2..071b480b1c 100644
--- a/packages/types/src/documents/app/rowAction.ts
+++ b/packages/types/src/documents/app/rowAction.ts
@@ -8,8 +8,10 @@ export interface TableRowActions extends Document {
 export interface RowActionData {
   name: string
   automationId: string
-  permissions: {
-    table: { runAllowed: boolean }
-    views: Record<string, { runAllowed: boolean }>
-  }
+  permissions: RowActionPermissions
+}
+
+export interface RowActionPermissions {
+  table: { runAllowed: boolean }
+  views: Record<string, { runAllowed: boolean }>
 }

From 75b5bbf4012a15a5a90359d348fa4a28242dd52c Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 13:21:28 +0200
Subject: [PATCH 03/10] Table run by default

---
 packages/server/src/sdk/app/rowActions.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts
index 21c256eacb..4a8f3afb28 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: false },
+      table: { runAllowed: true },
       views: {},
     },
   }

From 4c4f766a6a7fd3951c034694460196f07768ec2d Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 13:30:54 +0200
Subject: [PATCH 04/10] Endpoint to allow/disallow runs from table

---
 .../src/api/controllers/rowAction/crud.ts     | 31 +++++++++++++++++++
 packages/server/src/api/routes/rowAction.ts   | 10 ++++++
 packages/server/src/sdk/app/rowActions.ts     | 17 ++++++++++
 3 files changed, 58 insertions(+)

diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts
index 579f7e5f78..87a8cee909 100644
--- a/packages/server/src/api/controllers/rowAction/crud.ts
+++ b/packages/server/src/api/controllers/rowAction/crud.ts
@@ -95,6 +95,37 @@ export async function remove(ctx: Ctx<void, void>) {
   ctx.status = 204
 }
 
+export async function setTablePermission(ctx: Ctx<void, RowActionResponse>) {
+  const table = await getTable(ctx)
+  const tableId = table._id!
+  const { actionId } = ctx.params
+
+  const action = await sdk.rowActions.setTablePermission(tableId, actionId)
+  ctx.body = {
+    tableId,
+    id: action.id,
+    name: action.name,
+    automationId: action.automationId,
+    allowedSources: flattenAllowedSources(tableId, action.permissions),
+  }
+}
+
+export async function unsetTablePermission(ctx: Ctx<void, RowActionResponse>) {
+  const table = await getTable(ctx)
+  const tableId = table._id!
+  const { actionId } = ctx.params
+
+  const action = await sdk.rowActions.unsetTablePermission(tableId, actionId)
+
+  ctx.body = {
+    tableId,
+    id: action.id,
+    name: action.name,
+    automationId: action.automationId,
+    allowedSources: flattenAllowedSources(tableId, action.permissions),
+  }
+}
+
 export async function setViewPermission(ctx: Ctx<void, RowActionResponse>) {
   const table = await getTable(ctx)
   const tableId = table._id!
diff --git a/packages/server/src/api/routes/rowAction.ts b/packages/server/src/api/routes/rowAction.ts
index 54154e3ee8..3d14633509 100644
--- a/packages/server/src/api/routes/rowAction.ts
+++ b/packages/server/src/api/routes/rowAction.ts
@@ -51,6 +51,16 @@ router
     authorized(BUILDER),
     rowActionController.remove
   )
+  .post(
+    "/api/tables/:tableId/actions/:actionId/permissions",
+    authorized(BUILDER),
+    rowActionController.setTablePermission
+  )
+  .delete(
+    "/api/tables/:tableId/actions/:actionId/permissions",
+    authorized(BUILDER),
+    rowActionController.unsetTablePermission
+  )
   .post(
     "/api/tables/:tableId/actions/:actionId/permissions/:viewId",
     authorized(BUILDER),
diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts
index 4a8f3afb28..418a906c00 100644
--- a/packages/server/src/sdk/app/rowActions.ts
+++ b/packages/server/src/sdk/app/rowActions.ts
@@ -163,6 +163,23 @@ async function guardView(tableId: string, viewId: string) {
   }
 }
 
+export async function setTablePermission(tableId: string, rowActionId: string) {
+  return await updateDoc(tableId, rowActionId, async actionsDoc => {
+    actionsDoc.actions[rowActionId].permissions.table.runAllowed = true
+    return actionsDoc
+  })
+}
+
+export async function unsetTablePermission(
+  tableId: string,
+  rowActionId: string
+) {
+  return await updateDoc(tableId, rowActionId, async actionsDoc => {
+    actionsDoc.actions[rowActionId].permissions.table.runAllowed = false
+    return actionsDoc
+  })
+}
+
 export async function setViewPermission(
   tableId: string,
   rowActionId: string,

From 9063e73f886903b54ec1a5270b42807f72e16a90 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 13:51:45 +0200
Subject: [PATCH 05/10] Fix tests

---
 .../src/api/controllers/rowAction/crud.ts     |  4 ++--
 .../src/api/routes/tests/rowAction.spec.ts    | 24 ++++++++++++-------
 2 files changed, 17 insertions(+), 11 deletions(-)

diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts
index 87a8cee909..9a10147b09 100644
--- a/packages/server/src/api/controllers/rowAction/crud.ts
+++ b/packages/server/src/api/controllers/rowAction/crud.ts
@@ -62,7 +62,7 @@ export async function create(
     id: createdAction.id,
     name: createdAction.name,
     automationId: createdAction.automationId,
-    allowedSources: undefined,
+    allowedSources: flattenAllowedSources(tableId, createdAction.permissions),
   }
   ctx.status = 201
 }
@@ -83,7 +83,7 @@ export async function update(
     id: action.id,
     name: action.name,
     automationId: action.automationId,
-    allowedSources: undefined,
+    allowedSources: flattenAllowedSources(tableId, action.permissions),
   }
 }
 
diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index 7c1d88cc65..aad9074b31 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -133,6 +133,7 @@ describe("/rowsActions", () => {
         id: expect.stringMatching(/^row_action_\w+/),
         tableId: tableId,
         automationId: expectAutomationId(),
+        allowedSources: [tableId],
       })
 
       expect(await config.api.rowAction.find(tableId)).toEqual({
@@ -142,6 +143,7 @@ describe("/rowsActions", () => {
             id: res.id,
             tableId: tableId,
             automationId: expectAutomationId(),
+            allowedSources: [tableId],
           },
         },
       })
@@ -180,18 +182,21 @@ describe("/rowsActions", () => {
             id: responses[0].id,
             tableId,
             automationId: expectAutomationId(),
+            allowedSources: [tableId],
           },
           [responses[1].id]: {
             name: rowActions[1].name,
             id: responses[1].id,
             tableId,
             automationId: expectAutomationId(),
+            allowedSources: [tableId],
           },
           [responses[2].id]: {
             name: rowActions[2].name,
             id: responses[2].id,
             tableId,
             automationId: expectAutomationId(),
+            allowedSources: [tableId],
           },
         },
       })
@@ -224,6 +229,7 @@ describe("/rowsActions", () => {
         id: expect.any(String),
         tableId,
         automationId: expectAutomationId(),
+        allowedSources: [tableId],
       })
 
       expect(await config.api.rowAction.find(tableId)).toEqual({
@@ -233,6 +239,7 @@ describe("/rowsActions", () => {
             id: res.id,
             tableId: tableId,
             automationId: expectAutomationId(),
+            allowedSources: [tableId],
           },
         },
       })
@@ -354,6 +361,7 @@ describe("/rowsActions", () => {
         tableId,
         name: updatedName,
         automationId: actionData.automationId,
+        allowedSources: [tableId],
       })
 
       expect(await config.api.rowAction.find(tableId)).toEqual(
@@ -364,6 +372,7 @@ describe("/rowsActions", () => {
               id: actionData.id,
               tableId: actionData.tableId,
               automationId: actionData.automationId,
+              allowedSources: [tableId],
             },
           }),
         })
@@ -576,10 +585,10 @@ describe("/rowsActions", () => {
       )
 
       const expectedAction1 = expect.objectContaining({
-        allowedSources: [viewId1, viewId2],
+        allowedSources: [tableId, viewId1, viewId2],
       })
       const expectedAction2 = expect.objectContaining({
-        allowedSources: [viewId1],
+        allowedSources: [tableId, viewId1],
       })
 
       const expectedActions = expect.objectContaining({
@@ -601,7 +610,7 @@ describe("/rowsActions", () => {
       )
 
       const expectedAction = expect.objectContaining({
-        allowedSources: [viewId2],
+        allowedSources: [tableId, viewId2],
       })
       expect(actionResult).toEqual(expectedAction)
       expect(
@@ -901,7 +910,7 @@ describe("/rowsActions", () => {
       })
 
       it.each(allowedRoleConfig)(
-        "does not allow running row actions for tables by default even",
+        "allow running row actions for tables by default",
         async (userRole, resourcePermission) => {
           await config.api.permission.add({
             level: PermissionLevel.READ,
@@ -918,15 +927,12 @@ describe("/rowsActions", () => {
               rowAction.id,
               { rowId },
               {
-                status: 403,
-                body: {
-                  message: `Row action '${rowAction.id}' is not enabled for table '${tableId}'`,
-                },
+                status: 200,
               }
             )
 
             const automationLogs = await getAutomationLogs()
-            expect(automationLogs).toBeEmpty()
+            expect(automationLogs).toHaveLength(1)
           })
         }
       )

From bb6bd1711af275de743d2d77497e46ab1eb818d2 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 13:58:32 +0200
Subject: [PATCH 06/10] Add extra tests

---
 .../src/api/controllers/rowAction/crud.ts     |  3 -
 .../src/api/routes/tests/rowAction.spec.ts    | 82 +++++++++++++++++++
 .../src/tests/utilities/api/rowAction.ts      | 36 ++++++++
 3 files changed, 118 insertions(+), 3 deletions(-)

diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts
index 9a10147b09..67b6d9383e 100644
--- a/packages/server/src/api/controllers/rowAction/crud.ts
+++ b/packages/server/src/api/controllers/rowAction/crud.ts
@@ -178,9 +178,6 @@ function flattenAllowedSources(
       viewId => permissions.views[viewId].runAllowed
     )
   )
-  if (!allowedPermissions.length) {
-    return undefined
-  }
 
   return allowedPermissions
 }
diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index aad9074b31..bc21d199f7 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -524,6 +524,88 @@ describe("/rowsActions", () => {
     })
   })
 
+  describe("set/unsetTablePermission", () => {
+    describe.each([
+      ["setTablePermission", config.api.rowAction.setTablePermission],
+      ["unsetTablePermission", config.api.rowAction.unsetTablePermission],
+    ])("unauthorisedTests for %s", (__, delegateTest) => {
+      unauthorisedTests((expectations, testConfig) =>
+        delegateTest(tableId, generator.guid(), expectations, testConfig)
+      )
+    })
+
+    let tableIdForDescribe: string
+    let actionId1: string, actionId2: string
+
+    beforeAll(async () => {
+      tableIdForDescribe = tableId
+      for (const rowAction of createRowActionRequests(3)) {
+        await createRowAction(tableId, rowAction)
+      }
+      const persisted = await config.api.rowAction.find(tableId)
+
+      const actions = _.sampleSize(Object.keys(persisted.actions), 2)
+      actionId1 = actions[0]
+      actionId2 = actions[1]
+    })
+
+    beforeEach(() => {
+      // Hack to reuse tables for these given tests
+      tableId = tableIdForDescribe
+    })
+
+    it("can set table permission", async () => {
+      await config.api.rowAction.unsetTablePermission(tableId, actionId1)
+      await config.api.rowAction.unsetTablePermission(tableId, actionId2)
+      const actionResult = await config.api.rowAction.setTablePermission(
+        tableId,
+        actionId1
+      )
+      const expectedAction1 = expect.objectContaining({
+        allowedSources: [tableId],
+      })
+
+      const expectedActions = expect.objectContaining({
+        [actionId1]: expectedAction1,
+        [actionId2]: expect.objectContaining({
+          allowedSources: [],
+        }),
+      })
+      expect(actionResult).toEqual(expectedAction1)
+      expect((await config.api.rowAction.find(tableId)).actions).toEqual(
+        expectedActions
+      )
+    })
+
+    it("can unset table permission", async () => {
+      const actionResult = await config.api.rowAction.unsetTablePermission(
+        tableId,
+        actionId1
+      )
+
+      const expectedAction = expect.objectContaining({
+        allowedSources: [],
+      })
+      expect(actionResult).toEqual(expectedAction)
+      expect(
+        (await config.api.rowAction.find(tableId)).actions[actionId1]
+      ).toEqual(expectedAction)
+    })
+
+    it.each([
+      ["setTablePermission", config.api.rowAction.setTablePermission],
+      ["unsetTablePermission", config.api.rowAction.unsetTablePermission],
+    ])(
+      "cannot update permission for unexisting tables (%s)",
+      async (__, delegateTest) => {
+        const tableId = generator.guid()
+        await delegateTest(tableId, actionId1, {
+          status: 404,
+        })
+      }
+    )
+  })
+
   describe("set/unsetViewPermission", () => {
     describe.each([
       ["setViewPermission", config.api.rowAction.setViewPermission],
diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts
index 1adb60d235..0fd5936257 100644
--- a/packages/server/src/tests/utilities/api/rowAction.ts
+++ b/packages/server/src/tests/utilities/api/rowAction.ts
@@ -72,6 +72,42 @@ export class RowActionAPI extends TestAPI {
     )
   }
 
+  setTablePermission = async (
+    tableId: string,
+    rowActionId: string,
+    expectations?: Expectations,
+    config?: { publicUser?: boolean }
+  ) => {
+    return await this._post<RowActionResponse>(
+      `/api/tables/${tableId}/actions/${rowActionId}/permissions`,
+      {
+        expectations: {
+          status: 200,
+          ...expectations,
+        },
+        ...config,
+      }
+    )
+  }
+
+  unsetTablePermission = async (
+    tableId: string,
+    rowActionId: string,
+    expectations?: Expectations,
+    config?: { publicUser?: boolean }
+  ) => {
+    return await this._delete<RowActionResponse>(
+      `/api/tables/${tableId}/actions/${rowActionId}/permissions`,
+      {
+        expectations: {
+          status: 200,
+          ...expectations,
+        },
+        ...config,
+      }
+    )
+  }
+
   setViewPermission = async (
     tableId: string,
     viewId: string,

From 01b7f7388fc10844fab8adc08383d17a0d2048f2 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 14:16:50 +0200
Subject: [PATCH 07/10] More tests

---
 .../src/api/routes/tests/rowAction.spec.ts    | 32 +++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index bc21d199f7..f36681a147 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -803,6 +803,38 @@ describe("/rowsActions", () => {
       ])
     })
 
+    it("triggers from an allowed table", async () => {
+      expect(await getAutomationLogs()).toBeEmpty()
+      await config.api.rowAction.trigger(tableId, rowAction.id, { rowId })
+
+      const automationLogs = await getAutomationLogs()
+      expect(automationLogs).toEqual([
+        expect.objectContaining({
+          automationId: rowAction.automationId,
+        }),
+      ])
+    })
+
+    it("rejects triggering from a non-allowed table", async () => {
+      await config.api.rowAction.unsetTablePermission(tableId, rowAction.id)
+      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).toEqual([])
+    })
+
     it("rejects triggering from a non-allowed view", async () => {
       const viewId = (
         await config.api.viewV2.create(

From 3830d6cff7eb46b0107765c993bc01b2e4f9f564 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 14:45:11 +0200
Subject: [PATCH 08/10] Clean action between tests

---
 .../src/api/routes/tests/rowAction.spec.ts     | 18 ++----------------
 1 file changed, 2 insertions(+), 16 deletions(-)

diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index f36681a147..a373a35a66 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -534,11 +534,9 @@ describe("/rowsActions", () => {
       )
     })
 
-    let tableIdForDescribe: string
     let actionId1: string, actionId2: string
 
-    beforeAll(async () => {
-      tableIdForDescribe = tableId
+    beforeEach(async () => {
       for (const rowAction of createRowActionRequests(3)) {
         await createRowAction(tableId, rowAction)
       }
@@ -549,11 +547,6 @@ describe("/rowsActions", () => {
       actionId2 = actions[1]
     })
 
-    beforeEach(() => {
-      // Hack to reuse tables for these given tests
-      tableId = tableIdForDescribe
-    })
-
     it("can set table permission", async () => {
       await config.api.rowAction.unsetTablePermission(tableId, actionId1)
       await config.api.rowAction.unsetTablePermission(tableId, actionId2)
@@ -622,11 +615,9 @@ describe("/rowsActions", () => {
       )
     })
 
-    let tableIdForDescribe: string
     let actionId1: string, actionId2: string
     let viewId1: string, viewId2: string
-    beforeAll(async () => {
-      tableIdForDescribe = tableId
+    beforeEach(async () => {
       for (const rowAction of createRowActionRequests(3)) {
         await createRowAction(tableId, rowAction)
       }
@@ -648,11 +639,6 @@ describe("/rowsActions", () => {
       ).id
     })
 
-    beforeEach(() => {
-      // Hack to reuse tables for these given tests
-      tableId = tableIdForDescribe
-    })
-
     it("can set permission views", async () => {
       await config.api.rowAction.setViewPermission(tableId, viewId1, actionId1)
       const action1Result = await config.api.rowAction.setViewPermission(

From 9d7fdb1ed7d6ca8b022235c07e19e9b97d7f4380 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 14:45:28 +0200
Subject: [PATCH 09/10] Fix test

---
 packages/server/src/api/routes/tests/rowAction.spec.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index a373a35a66..6d1de3e4cc 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -671,6 +671,8 @@ describe("/rowsActions", () => {
     })
 
     it("can unset permission views", async () => {
+      await config.api.rowAction.setViewPermission(tableId, viewId2, actionId1)
+      await config.api.rowAction.setViewPermission(tableId, viewId1, actionId2)
       const actionResult = await config.api.rowAction.unsetViewPermission(
         tableId,
         viewId1,

From f38180eaaac133f7b6341de5704bb793ec8a8517 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Fri, 4 Oct 2024 14:48:50 +0200
Subject: [PATCH 10/10] Add comment

---
 packages/server/src/api/routes/tests/rowAction.spec.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index 6d1de3e4cc..fb59ce73c3 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -751,6 +751,7 @@ describe("/rowsActions", () => {
       )
 
       await config.publish()
+      // Travel time in order to "trim" the selected `getAutomationLogs`
       tk.travel(Date.now() + 100)
     })