diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index 6177868303..b19218647b 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -159,6 +159,7 @@ export async function trigger(ctx: UserCtx) { automation, { fields: ctx.request.body.fields, + user: sdk.users.getUserContextBindings(ctx.user), timeout: ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT, }, @@ -183,6 +184,7 @@ export async function trigger(ctx: UserCtx) { await triggers.externalTrigger(automation, { ...ctx.request.body, appId: ctx.appId, + user: sdk.users.getUserContextBindings(ctx.user), }) ctx.body = { message: `Automation ${automation._id} has been triggered.`, @@ -212,6 +214,7 @@ export async function test(ctx: UserCtx) { { ...testInput, appId: ctx.appId, + user: sdk.users.getUserContextBindings(ctx.user), }, { getResponses: true } ) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 60775ce628..4d40476b7c 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -65,7 +65,14 @@ export async function patch( } ctx.status = 200 ctx.eventEmitter && - ctx.eventEmitter.emitRow(`row:update`, appId, row, table, oldRow) + ctx.eventEmitter.emitRow({ + eventName: `row:update`, + appId, + row, + table, + oldRow, + user: sdk.users.getUserContextBindings(ctx.user), + }) ctx.message = `${table.name} updated successfully.` ctx.body = row gridSocket?.emitRowUpdate(ctx, row) @@ -96,7 +103,14 @@ export const save = async (ctx: UserCtx) => { sdk.rows.save(sourceId, ctx.request.body, ctx.user?._id) ) ctx.status = 200 - ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table) + ctx.eventEmitter && + ctx.eventEmitter.emitRow({ + eventName: `row:save`, + appId, + row, + table, + user: sdk.users.getUserContextBindings(ctx.user), + }) ctx.message = `${table.name} saved successfully` // prefer squashed for response ctx.body = row || squashed @@ -168,10 +182,15 @@ async function deleteRows(ctx: UserCtx) { } for (let row of rows) { - ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) + ctx.eventEmitter && + ctx.eventEmitter.emitRow({ + eventName: `row:delete`, + appId, + row, + user: sdk.users.getUserContextBindings(ctx.user), + }) gridSocket?.emitRowDeletion(ctx, row) } - return rows } @@ -184,7 +203,13 @@ async function deleteRow(ctx: UserCtx) { await quotas.removeRow() } - ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row) + ctx.eventEmitter && + ctx.eventEmitter.emitRow({ + eventName: `row:delete`, + appId, + row: resp.row, + user: sdk.users.getUserContextBindings(ctx.user), + }) gridSocket?.emitRowDeletion(ctx, resp.row) return resp diff --git a/packages/server/src/api/controllers/rowAction/run.ts b/packages/server/src/api/controllers/rowAction/run.ts index c4b6a276bf..05bdb7bace 100644 --- a/packages/server/src/api/controllers/rowAction/run.ts +++ b/packages/server/src/api/controllers/rowAction/run.ts @@ -5,6 +5,6 @@ export async function run(ctx: Ctx) { const { tableId, actionId } = ctx.params const { rowId } = ctx.request.body - await sdk.rowActions.run(tableId, actionId, rowId) + await sdk.rowActions.run(tableId, actionId, rowId, ctx.user) ctx.status = 200 } diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 7140f3486e..9f9a33c73b 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -767,7 +767,6 @@ describe("/rowsActions", () => { it("can trigger an automation given valid data", async () => { expect(await getAutomationLogs()).toBeEmpty() await config.api.rowAction.trigger(viewId, rowAction.id, { rowId }) - const automationLogs = await getAutomationLogs() expect(automationLogs).toEqual([ expect.objectContaining({ @@ -783,6 +782,10 @@ describe("/rowsActions", () => { ...(await config.api.table.get(tableId)), views: expect.anything(), }, + user: expect.objectContaining({ + _id: "ro_ta_users_" + config.getUser()._id, + }), + automation: expect.objectContaining({ _id: rowAction.automationId, }), diff --git a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts index 850a04a95a..358aba35c8 100644 --- a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts @@ -482,4 +482,38 @@ describe("Automation Scenarios", () => { } ) }) + + it("Check user is passed through from row trigger", async () => { + const table = await config.createTable() + + const builder = createAutomationBuilder({ + name: "Test a user is successfully passed from the trigger", + }) + + const results = await builder + .rowUpdated( + { tableId: table._id! }, + { + row: { name: "Test", description: "TEST" }, + id: "1234", + } + ) + .serverLog({ text: "{{ [user].[email] }}" }) + .run() + + expect(results.steps[0].outputs.message).toContain("example.com") + }) + + it("Check user is passed through from app trigger", async () => { + const builder = createAutomationBuilder({ + name: "Test a user is successfully passed from the trigger", + }) + + const results = await builder + .appAction({ fields: {} }) + .serverLog({ text: "{{ [user].[email] }}" }) + .run() + + expect(results.steps[0].outputs.message).toContain("example.com") + }) }) diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index 110ccfa37a..70fda1f237 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -19,6 +19,7 @@ import { AutomationStoppedReason, AutomationStatus, AutomationRowEvent, + UserBindings, } from "@budibase/types" import { executeInThread } from "../threads/automation" import { dataFilters, sdk } from "@budibase/shared-core" @@ -140,7 +141,12 @@ function rowPassesFilters(row: Row, filters: SearchFilters) { export async function externalTrigger( automation: Automation, - params: { fields: Record; timeout?: number; appId?: string }, + params: { + fields: Record + timeout?: number + appId?: string + user?: UserBindings + }, { getResponses }: { getResponses?: boolean } = {} ): Promise { if (automation.disabled) { diff --git a/packages/server/src/definitions/automations.ts b/packages/server/src/definitions/automations.ts index 9433075da7..d551584fd1 100644 --- a/packages/server/src/definitions/automations.ts +++ b/packages/server/src/definitions/automations.ts @@ -1,4 +1,4 @@ -import { AutomationResults, LoopStepType } from "@budibase/types" +import { AutomationResults, LoopStepType, UserBindings } from "@budibase/types" export interface LoopInput { option: LoopStepType @@ -18,5 +18,6 @@ export interface AutomationContext extends AutomationResults { stepsById: Record stepsByName: Record env?: Record + user?: UserBindings trigger: any } diff --git a/packages/server/src/events/AutomationEmitter.ts b/packages/server/src/events/AutomationEmitter.ts index 32fd130929..a63273bdc0 100644 --- a/packages/server/src/events/AutomationEmitter.ts +++ b/packages/server/src/events/AutomationEmitter.ts @@ -31,7 +31,17 @@ class AutomationEmitter { } } - async emitRow(eventName: string, appId: string, row: Row, table?: Table) { + async emitRow({ + eventName, + appId, + row, + table, + }: { + eventName: string + appId: string + row: Row + table?: Table + }) { let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain() // don't emit even if we've reached max automation chain diff --git a/packages/server/src/events/BudibaseEmitter.ts b/packages/server/src/events/BudibaseEmitter.ts index 8feb36bbf5..c8983096d0 100644 --- a/packages/server/src/events/BudibaseEmitter.ts +++ b/packages/server/src/events/BudibaseEmitter.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "events" import { rowEmission, tableEmission } from "./utils" -import { Table, Row } from "@budibase/types" +import { Table, Row, User } from "@budibase/types" /** * keeping event emitter in one central location as it might be used for things other than @@ -13,14 +13,22 @@ import { Table, Row } from "@budibase/types" * This is specifically quite important for template strings used in automations. */ class BudibaseEmitter extends EventEmitter { - emitRow( - eventName: string, - appId: string, - row: Row, - table?: Table, + emitRow({ + eventName, + appId, + row, + table, + oldRow, + user, + }: { + eventName: string + appId: string + row: Row + table?: Table oldRow?: Row - ) { - rowEmission({ emitter: this, eventName, appId, row, table, oldRow }) + user: User + }) { + rowEmission({ emitter: this, eventName, appId, row, table, oldRow, user }) } emitTable(eventName: string, appId: string, table?: Table) { diff --git a/packages/server/src/events/utils.ts b/packages/server/src/events/utils.ts index b972c8e473..5e4a1bebbf 100644 --- a/packages/server/src/events/utils.ts +++ b/packages/server/src/events/utils.ts @@ -1,4 +1,4 @@ -import { Table, Row } from "@budibase/types" +import { Table, Row, User } from "@budibase/types" import BudibaseEmitter from "./BudibaseEmitter" type BBEventOpts = { @@ -9,6 +9,7 @@ type BBEventOpts = { row?: Row oldRow?: Row metadata?: any + user?: User } interface BBEventTable extends Table { @@ -24,6 +25,7 @@ type BBEvent = { id?: string revision?: string metadata?: any + user?: User } export function rowEmission({ @@ -34,12 +36,14 @@ export function rowEmission({ table, metadata, oldRow, + user, }: BBEventOpts) { let event: BBEvent = { row, oldRow, appId, tableId: row?.tableId, + user, } if (table) { event.table = table diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index 3d8a6fd9be..ed87133b5d 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -4,6 +4,7 @@ import { AutomationTriggerStepId, SEPARATOR, TableRowActions, + User, VirtualDocumentType, } from "@budibase/types" import { generateRowActionsID } from "../../db/utils" @@ -236,7 +237,12 @@ export async function remove(tableId: string, rowActionId: string) { }) } -export async function run(tableId: any, rowActionId: any, rowId: string) { +export async function run( + tableId: any, + rowActionId: any, + rowId: string, + user: User +) { const table = await sdk.tables.getTable(tableId) if (!table) { throw new HTTPError("Table not found", 404) @@ -258,6 +264,7 @@ export async function run(tableId: any, rowActionId: any, rowId: string) { row, table, }, + user, appId: context.getAppId(), }, { getResponses: true } diff --git a/packages/server/src/sdk/users/utils.ts b/packages/server/src/sdk/users/utils.ts index 74389a1444..8266d3e28a 100644 --- a/packages/server/src/sdk/users/utils.ts +++ b/packages/server/src/sdk/users/utils.ts @@ -12,6 +12,7 @@ import { UserMetadata, Database, ContextUserMetadata, + UserBindings, } from "@budibase/types" export function combineMetadataAndUser( @@ -125,7 +126,7 @@ export async function syncGlobalUsers() { } } -export function getUserContextBindings(user: ContextUser) { +export function getUserContextBindings(user: ContextUser): UserBindings { if (!user) { return {} } diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 3b47634663..0f17b44424 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -26,6 +26,7 @@ import { BranchStep, LoopStep, SearchFilters, + UserBindings, } from "@budibase/types" import { AutomationContext, TriggerOutput } from "../definitions/automations" import { WorkerCallback } from "./definitions" @@ -75,6 +76,7 @@ class Orchestrator { private loopStepOutputs: LoopStep[] private stopped: boolean private executionOutput: Omit + private currentUser: UserBindings | undefined constructor(job: AutomationJob) { let automation = job.data.automation @@ -106,6 +108,7 @@ class Orchestrator { this.updateExecutionOutput(triggerId, triggerStepId, null, triggerOutput) this.loopStepOutputs = [] this.stopped = false + this.currentUser = triggerOutput.user } cleanupTriggerOutputs(stepId: string, triggerOutput: TriggerOutput) { @@ -258,6 +261,7 @@ class Orchestrator { automationId: this.automation._id, }) this.context.env = await sdkUtils.getEnvironmentVariables() + this.context.user = this.currentUser let metadata @@ -572,7 +576,6 @@ class Orchestrator { originalStepInput, this.processContext(this.context) ) - inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs) const outputs = await stepFn({ diff --git a/packages/types/src/documents/app/automation/automation.ts b/packages/types/src/documents/app/automation/automation.ts index effe99a328..1af892d8d1 100644 --- a/packages/types/src/documents/app/automation/automation.ts +++ b/packages/types/src/documents/app/automation/automation.ts @@ -261,6 +261,7 @@ export type UpdatedRowEventEmitter = { oldRow: Row table: Table appId: string + user: User } export enum LoopStepType { diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index a1c5b2506f..829a171843 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -68,6 +68,16 @@ export interface User extends Document { appSort?: string } +export interface UserBindings extends Document { + firstName?: string + lastName?: string + email?: string + status?: string + roleId?: string | null + globalId?: string + userId?: string +} + export enum UserStatus { ACTIVE = "active", INACTIVE = "inactive", diff --git a/packages/types/src/sdk/automations/index.ts b/packages/types/src/sdk/automations/index.ts index d04f126c32..9ceded03ee 100644 --- a/packages/types/src/sdk/automations/index.ts +++ b/packages/types/src/sdk/automations/index.ts @@ -1,4 +1,9 @@ -import { Automation, AutomationMetadata, Row } from "../../documents" +import { + Automation, + AutomationMetadata, + Row, + UserBindings, +} from "../../documents" import { Job } from "bull" export interface AutomationDataEvent { @@ -8,6 +13,7 @@ export interface AutomationDataEvent { timeout?: number row?: Row oldRow?: Row + user?: UserBindings } export interface AutomationData {