From 5e69d84aecb9e9376086ea5f01801659df7bb58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 13 Jun 2024 10:23:07 +0200 Subject: [PATCH 01/25] refactor(core): Decouple event bus from internal hooks (no-changelog) --- packages/cli/src/decorators/Redacted.ts | 35 ++ packages/cli/src/decorators/index.ts | 1 + .../__tests__/auditor.service.test.ts | 37 ++ packages/cli/src/eventbus/audit.types.ts | 181 +++++++++ packages/cli/src/eventbus/auditor.service.ts | 349 ++++++++++++++++++ packages/cli/src/eventbus/event-sender.ts | 16 + 6 files changed, 619 insertions(+) create mode 100644 packages/cli/src/decorators/Redacted.ts create mode 100644 packages/cli/src/eventbus/__tests__/auditor.service.test.ts create mode 100644 packages/cli/src/eventbus/audit.types.ts create mode 100644 packages/cli/src/eventbus/auditor.service.ts create mode 100644 packages/cli/src/eventbus/event-sender.ts diff --git a/packages/cli/src/decorators/Redacted.ts b/packages/cli/src/decorators/Redacted.ts new file mode 100644 index 0000000000000..cddaaee8062b8 --- /dev/null +++ b/packages/cli/src/decorators/Redacted.ts @@ -0,0 +1,35 @@ +import type { UserLike } from '@/eventbus/audit.types'; + +export function redact(userLike: UserLike) { + delete userLike.email; + delete userLike.firstName; + delete userLike.lastName; + + return userLike; +} + +type Redactable = 'user' | 'inviter' | 'invitee'; + +/** + * Redact sensitive properties in a `UserLike` field in an event bus arg. + */ +export const Redacted = + (fieldName: Redactable = 'user'): MethodDecorator => + (_target, _propertyName, propertyDescriptor: PropertyDescriptor) => { + const originalMethod = propertyDescriptor.value as Function; + + type MethodArgs = Array<{ [fieldName: string]: UserLike }>; + + propertyDescriptor.value = function (...args: MethodArgs) { + const index = args.findIndex((arg) => arg[fieldName] !== undefined); + + const userLike = args[index]?.[fieldName]; + + if (userLike) args[index][fieldName] = redact(userLike); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return originalMethod.apply(this, args); + }; + + return propertyDescriptor; + }; diff --git a/packages/cli/src/decorators/index.ts b/packages/cli/src/decorators/index.ts index 576b55cdd77e4..a5e1068d17040 100644 --- a/packages/cli/src/decorators/index.ts +++ b/packages/cli/src/decorators/index.ts @@ -4,3 +4,4 @@ export { Middleware } from './Middleware'; export { registerController } from './registerController'; export { Licensed } from './Licensed'; export { GlobalScope, ProjectScope } from './Scoped'; +export { Redacted } from './Redacted'; diff --git a/packages/cli/src/eventbus/__tests__/auditor.service.test.ts b/packages/cli/src/eventbus/__tests__/auditor.service.test.ts new file mode 100644 index 0000000000000..6d7d765a66142 --- /dev/null +++ b/packages/cli/src/eventbus/__tests__/auditor.service.test.ts @@ -0,0 +1,37 @@ +import { mock } from 'jest-mock-extended'; +import type { MessageEventBus } from '../MessageEventBus/MessageEventBus'; +import { AuditorService } from '../auditor.service'; +import type { AuditEventArgs } from '../audit.types'; + +describe('AuditorService', () => { + const eventBus = mock(); + const auditor = new AuditorService(eventBus); + + it('should handle `user-deleted` event', () => { + const arg: AuditEventArgs['user-deleted'] = { + user: { + id: '123', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'some-role', + }, + }; + + // @ts-expect-error Private method + auditor.userDeleted(arg); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.deleted', + payload: { + user: { + id: arg.user.id, + email: undefined, + firstName: undefined, + lastName: undefined, + role: arg.user.role, + }, + }, + }); + }); +}); diff --git a/packages/cli/src/eventbus/audit.types.ts b/packages/cli/src/eventbus/audit.types.ts new file mode 100644 index 0000000000000..4488c0931a15f --- /dev/null +++ b/packages/cli/src/eventbus/audit.types.ts @@ -0,0 +1,181 @@ +import type { AuthenticationMethod, IWorkflowBase } from 'n8n-workflow'; +import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; + +export type UserLike = { + id: string; + email?: string; + firstName?: string; + lastName?: string; + role: string; +}; + +export type AuditEventArgs = { + 'workflow-created': { + user: UserLike; + workflow: IWorkflowBase; + }; + + 'workflow-deleted': { + user: UserLike; + workflow: IWorkflowBase; + }; + + 'workflow-saved': { + user: UserLike; + workflow: IWorkflowBase; + }; + + 'workflow-pre-execute': { + executionId: string; + data: IWorkflowExecutionDataProcess /* main process */ | IWorkflowBase /* worker */; + }; + + 'workflow-post-execute': { + executionId: string; + success: boolean; + userId: string; + workflowId: string; + isManual: boolean; + workflowName: string; + metadata?: Record; + }; + + 'node-pre-execute': { + executionId: string; + workflow: IWorkflowBase; + nodeName: string; + }; + + 'node-post-execute': { + executionId: string; + workflow: IWorkflowBase; + nodeName: string; + }; + + 'user-deleted': { + user: UserLike; + }; + + 'user-invited': { + user: UserLike; + targetUserId: string[]; + }; + + 'user-reinvited': { + user: UserLike; + targetUserId: string[]; + }; + + 'user-updated': { + user: UserLike; + fieldsChanged: string[]; + }; + + 'user-signed-up': { + user: UserLike; + }; + + 'user-logged-in': { + user: UserLike; + authenticationMethod: AuthenticationMethod; + }; + + 'user-login-failed': { + user: UserLike; + authenticationMethod: AuthenticationMethod; + reason?: string; + }; + + 'user-invite-email-click': { + inviter: UserLike; + invitee: UserLike; + }; + + 'user-password-reset-email-click': { + user: UserLike; + }; + + 'user-password-reset-request-click': { + user: UserLike; + }; + + 'api-key-created': { + user: UserLike; + }; + + 'api-key-deleted': { + user: UserLike; + }; + + 'email-failed': { + user: UserLike; + messageType: + | 'Reset password' + | 'New user invite' + | 'Resend invite' + | 'Workflow shared' + | 'Credentials shared'; + }; + + 'credentials-created': { + user: UserLike; + credentialName: string; + credentialType: string; + credentialId: string; + }; + + 'credentials-shared': { + user: UserLike; + credentialName: string; + credentialType: string; + credentialId: string; + userIdSharer: string; + userIdsShareesRemoved: string[]; + shareesRemoved: number | null; + }; + + 'credentials-updated': { + user: UserLike; + credentialName: string; + credentialType: string; + credentialId: string; + }; + + 'credentials-deleted': { + user: UserLike; + credentialName: string; + credentialType: string; + credentialId: string; + }; + + 'community-package-installed': { + user: UserLike; + inputString: string; + packageName: string; + success: boolean; + packageVersion: string; + packageNodeNames: string[]; + packageAuthor: string; + packageAuthorEmail: string; + failureReason?: string; + }; + + 'community-package-updated': { + user: UserLike; + packageName: string; + packageVersionCurrent: string; + packageVersionNew: string; + packageNodeNames: string[]; + packageAuthor?: string; + packageAuthorEmail?: string; + }; + + 'community-package-deleted': { + user: UserLike; + packageName: string; + packageVersion: string; + packageNodeNames: string[]; + packageAuthor?: string; + packageAuthorEmail?: string; + }; +}; diff --git a/packages/cli/src/eventbus/auditor.service.ts b/packages/cli/src/eventbus/auditor.service.ts new file mode 100644 index 0000000000000..ab98dcaac6fed --- /dev/null +++ b/packages/cli/src/eventbus/auditor.service.ts @@ -0,0 +1,349 @@ +import { EventEmitter } from 'node:events'; +import { Service } from 'typedi'; +import { MessageEventBus } from './MessageEventBus/MessageEventBus'; +import { Redacted } from '@/decorators/Redacted'; +import type { AuditEventArgs } from './audit.types'; +import type { IWorkflowBase } from 'n8n-workflow'; + +// @TODO: Emit all these events +// @TODO: Remove eventbus from InternalHooks + +/** + * Service for adding events from `EventSender` to the audit log. + */ +@Service() +export class AuditorService extends EventEmitter { + constructor(private readonly eventBus: MessageEventBus) { + super(); + this.setupHandlers(); + } + + on(eventName: K, handler: (arg: AuditEventArgs[K]) => void) { + super.on(eventName, handler); + return this; + } + + private setupHandlers() { + this.on('workflow-created', (arg) => this.workflowCreated(arg)); + this.on('workflow-deleted', (arg) => this.workflowDeleted(arg)); + this.on('workflow-saved', (arg) => this.workflowSaved(arg)); + this.on('workflow-pre-execute', (arg) => this.workflowPreExecute(arg)); + this.on('workflow-post-execute', (arg) => this.workflowPostExecute(arg)); + this.on('node-pre-execute', (arg) => this.nodePreExecute(arg)); + this.on('node-post-execute', (arg) => this.nodePostExecute(arg)); + this.on('user-deleted', (arg) => this.userDeleted(arg)); + this.on('user-invited', (arg) => this.userInvited(arg)); + this.on('user-reinvited', (arg) => this.userReinvited(arg)); + this.on('user-updated', (arg) => this.userUpdated(arg)); + this.on('user-signed-up', (arg) => this.userSignedUp(arg)); + this.on('user-logged-in', (arg) => this.userLoggedIn(arg)); + this.on('user-login-failed', (arg) => this.userLoginFailed(arg)); + this.on('user-invite-email-click', (arg) => this.userInviteEmailClick(arg)); + this.on('user-password-reset-email-click', (arg) => this.userPasswordResetEmailClick(arg)); + this.on('user-password-reset-request-click', (arg) => this.userPasswordResetRequestClick(arg)); + this.on('api-key-created', (arg) => this.apiKeyCreated(arg)); + this.on('api-key-deleted', (arg) => this.apiKeyDeleted(arg)); + this.on('email-failed', (arg) => this.emailFailed(arg)); + this.on('credentials-created', (arg) => this.credentialsCreated(arg)); + this.on('credentials-deleted', (arg) => this.credentialsDeleted(arg)); + this.on('credentials-shared', (arg) => this.credentialsShared(arg)); + this.on('credentials-updated', (arg) => this.credentialsUpdated(arg)); + this.on('credentials-deleted', (arg) => this.credentialsDeleted(arg)); + this.on('community-package-installed', (arg) => this.communityPackageInstalled(arg)); + this.on('community-package-updated', (arg) => this.communityPackageUpdated(arg)); + this.on('community-package-deleted', (arg) => this.communityPackageDeleted(arg)); + } + + /** + * Workflow + */ + + @Redacted() + private workflowCreated({ user, workflow }: AuditEventArgs['workflow-created']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.workflow.created', + payload: { + ...user, + workflowId: workflow.id, + workflowName: workflow.name, + }, + }); + } + + @Redacted() + private workflowDeleted({ user, workflow }: AuditEventArgs['workflow-deleted']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.workflow.deleted', + payload: { ...user, workflow: workflow.id }, + }); + } + + @Redacted() + private workflowSaved({ user, workflow }: AuditEventArgs['workflow-saved']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.workflow.updated', + payload: { + ...user, + workflowId: workflow.id, + workflowName: workflow.name, + }, + }); + } + + private workflowPreExecute({ data, executionId }: AuditEventArgs['workflow-pre-execute']) { + const payload = + 'executionData' in data + ? { + executionId, + userId: data.userId, + workflowId: data.workflowData.id, + isManual: data.executionMode === 'manual', + workflowName: data.workflowData.name, + } + : { + executionId, + userId: undefined, + workflowId: (data as IWorkflowBase).id, + isManual: false, + workflowName: (data as IWorkflowBase).name, + }; + + void this.eventBus.sendWorkflowEvent({ + eventName: 'n8n.workflow.started', + payload, + }); + } + + private workflowPostExecute(arg: AuditEventArgs['workflow-post-execute']) { + void this.eventBus.sendWorkflowEvent({ + eventName: 'n8n.workflow.success', + payload: arg, + }); + } + + /** + * Node + */ + + private nodePreExecute({ executionId, nodeName, workflow }: AuditEventArgs['node-pre-execute']) { + void this.eventBus.sendNodeEvent({ + eventName: 'n8n.node.started', + payload: { + executionId, + nodeName, + workflowId: workflow.id, + workflowName: workflow.name, + nodeType: workflow.nodes.find((n) => n.name === nodeName)?.type, + }, + }); + } + + private nodePostExecute({ + executionId, + nodeName, + workflow, + }: AuditEventArgs['node-post-execute']) { + void this.eventBus.sendNodeEvent({ + eventName: 'n8n.node.finished', + payload: { + executionId, + nodeName, + workflowId: workflow.id, + workflowName: workflow.name, + nodeType: workflow.nodes.find((n) => n.name === nodeName)?.type, + }, + }); + } + + /** + * User + */ + + @Redacted() + private userDeleted(arg: AuditEventArgs['user-deleted']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.deleted', + payload: arg, + }); + } + + @Redacted() + private userInvited({ user, targetUserId }: AuditEventArgs['user-invited']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.invited', + payload: { ...user, targetUserId }, + }); + } + + @Redacted() + private userReinvited({ user, targetUserId }: AuditEventArgs['user-reinvited']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.reinvited', + payload: { ...user, targetUserId }, + }); + } + + @Redacted() + private userUpdated({ user, fieldsChanged }: AuditEventArgs['user-updated']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.updated', + payload: { ...user, fieldsChanged }, + }); + } + + /** + * Auth + */ + + @Redacted() + private userSignedUp(arg: AuditEventArgs['user-signed-up']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.signedup', + payload: arg, + }); + } + + @Redacted() + private userLoggedIn({ user, authenticationMethod }: AuditEventArgs['user-logged-in']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.login.success', + payload: { ...user, authenticationMethod }, + }); + } + + @Redacted() + private userLoginFailed({ user, ...rest }: AuditEventArgs['user-login-failed']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.login.failed', + payload: { ...user, ...rest }, + }); + } + + /** + * Click + */ + + @Redacted('inviter') + @Redacted('invitee') + private userInviteEmailClick(arg: AuditEventArgs['user-invite-email-click']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.invitation.accepted', + payload: arg, + }); + } + + @Redacted() + private userPasswordResetEmailClick(arg: AuditEventArgs['user-password-reset-email-click']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.reset', + payload: arg, + }); + } + + @Redacted() + private userPasswordResetRequestClick(arg: AuditEventArgs['user-password-reset-request-click']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.reset.requested', + payload: arg, + }); + } + + /** + * API key + */ + + @Redacted() + private apiKeyCreated(arg: AuditEventArgs['api-key-created']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.api.created', + payload: arg, + }); + } + + @Redacted() + private apiKeyDeleted(arg: AuditEventArgs['api-key-deleted']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.api.deleted', + payload: arg, + }); + } + + /** + * Emailing + */ + + @Redacted() + private emailFailed({ user, messageType }: AuditEventArgs['email-failed']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.email.failed', + payload: { ...user, messageType }, + }); + } + + /** + * Credentials + */ + + @Redacted() + private credentialsCreated({ user, ...rest }: AuditEventArgs['credentials-created']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.credentials.created', + payload: { ...user, ...rest }, + }); + } + + @Redacted() + private credentialsDeleted({ user, ...rest }: AuditEventArgs['credentials-deleted']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.credentials.deleted', + payload: { ...user, ...rest }, + }); + } + + @Redacted() + private credentialsShared({ user, ...rest }: AuditEventArgs['credentials-shared']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.credentials.shared', + payload: { ...user, ...rest }, + }); + } + + @Redacted() + private credentialsUpdated({ user, ...rest }: AuditEventArgs['credentials-updated']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.credentials.updated', + payload: { ...user, ...rest }, + }); + } + + /** + * Community package + */ + + @Redacted() + private communityPackageInstalled({ + user, + ...rest + }: AuditEventArgs['community-package-installed']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.package.installed', + payload: { ...user, ...rest }, + }); + } + + @Redacted() + private communityPackageUpdated({ user, ...rest }: AuditEventArgs['community-package-updated']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.package.updated', + payload: { ...user, ...rest }, + }); + } + + @Redacted() + private communityPackageDeleted({ user, ...rest }: AuditEventArgs['community-package-deleted']) { + void this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.package.deleted', + payload: { ...user, ...rest }, + }); + } +} diff --git a/packages/cli/src/eventbus/event-sender.ts b/packages/cli/src/eventbus/event-sender.ts new file mode 100644 index 0000000000000..4388394ca6a60 --- /dev/null +++ b/packages/cli/src/eventbus/event-sender.ts @@ -0,0 +1,16 @@ +import { EventEmitter } from 'node:events'; +import type { AuditEventArgs } from './audit.types'; +import { Service } from 'typedi'; + +type EventArgs = AuditEventArgs; // also `TelemetryEventArgs` in future + +/** + * Service to send events for other services to listen to. + */ +@Service() +export class EventSender extends EventEmitter { + emit(eventName: K, arg: EventArgs[K]) { + super.emit(eventName, arg); + return true; + } +} From b1969588c995ae1e5b719922d9eca27907e8151d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 12:05:54 +0200 Subject: [PATCH 02/25] Rename to `Redactable` --- packages/cli/src/decorators/Redactable.ts | 40 ++++++++++++++++ packages/cli/src/decorators/Redacted.ts | 35 -------------- packages/cli/src/decorators/index.ts | 1 - packages/cli/src/eventbus/auditor.service.ts | 50 ++++++++++---------- 4 files changed, 65 insertions(+), 61 deletions(-) create mode 100644 packages/cli/src/decorators/Redactable.ts delete mode 100644 packages/cli/src/decorators/Redacted.ts diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts new file mode 100644 index 0000000000000..e83026aba2162 --- /dev/null +++ b/packages/cli/src/decorators/Redactable.ts @@ -0,0 +1,40 @@ +import type { UserLike } from '@/eventbus/audit.types'; + +function markRedactable(userLike: UserLike) { + return { + userId: userLike.id, + _email: userLike.email, + _firstName: userLike.firstName, + _lastName: userLike.lastName, + globalRole: userLike.role, + }; +} + +type FieldName = 'user' | 'inviter' | 'invitee'; + +/** + * Mark redactable properties in a `UserLike` field in an event bus arg. + * These properties will be redacted by the log streaming destination + * if so requested by the user. + */ +export const Redactable = + (fieldName: FieldName = 'user'): MethodDecorator => + (_target, _propertyName, propertyDescriptor: PropertyDescriptor) => { + const originalMethod = propertyDescriptor.value as Function; + + type MethodArgs = Array<{ [fieldName: string]: UserLike }>; + + propertyDescriptor.value = function (...args: MethodArgs) { + const index = args.findIndex((arg) => arg[fieldName] !== undefined); + + const userLike = args[index]?.[fieldName]; + + // @ts-expect-error Sensitive properties prepended with underscore + if (userLike) args[index][fieldName] = markRedactable(userLike); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return originalMethod.apply(this, args); + }; + + return propertyDescriptor; + }; diff --git a/packages/cli/src/decorators/Redacted.ts b/packages/cli/src/decorators/Redacted.ts deleted file mode 100644 index cddaaee8062b8..0000000000000 --- a/packages/cli/src/decorators/Redacted.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { UserLike } from '@/eventbus/audit.types'; - -export function redact(userLike: UserLike) { - delete userLike.email; - delete userLike.firstName; - delete userLike.lastName; - - return userLike; -} - -type Redactable = 'user' | 'inviter' | 'invitee'; - -/** - * Redact sensitive properties in a `UserLike` field in an event bus arg. - */ -export const Redacted = - (fieldName: Redactable = 'user'): MethodDecorator => - (_target, _propertyName, propertyDescriptor: PropertyDescriptor) => { - const originalMethod = propertyDescriptor.value as Function; - - type MethodArgs = Array<{ [fieldName: string]: UserLike }>; - - propertyDescriptor.value = function (...args: MethodArgs) { - const index = args.findIndex((arg) => arg[fieldName] !== undefined); - - const userLike = args[index]?.[fieldName]; - - if (userLike) args[index][fieldName] = redact(userLike); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return originalMethod.apply(this, args); - }; - - return propertyDescriptor; - }; diff --git a/packages/cli/src/decorators/index.ts b/packages/cli/src/decorators/index.ts index a5e1068d17040..576b55cdd77e4 100644 --- a/packages/cli/src/decorators/index.ts +++ b/packages/cli/src/decorators/index.ts @@ -4,4 +4,3 @@ export { Middleware } from './Middleware'; export { registerController } from './registerController'; export { Licensed } from './Licensed'; export { GlobalScope, ProjectScope } from './Scoped'; -export { Redacted } from './Redacted'; diff --git a/packages/cli/src/eventbus/auditor.service.ts b/packages/cli/src/eventbus/auditor.service.ts index ab98dcaac6fed..0bc3792b267fd 100644 --- a/packages/cli/src/eventbus/auditor.service.ts +++ b/packages/cli/src/eventbus/auditor.service.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'node:events'; import { Service } from 'typedi'; import { MessageEventBus } from './MessageEventBus/MessageEventBus'; -import { Redacted } from '@/decorators/Redacted'; +import { Redactable } from '@/decorators/Redactable'; import type { AuditEventArgs } from './audit.types'; import type { IWorkflowBase } from 'n8n-workflow'; @@ -58,7 +58,7 @@ export class AuditorService extends EventEmitter { * Workflow */ - @Redacted() + @Redactable() private workflowCreated({ user, workflow }: AuditEventArgs['workflow-created']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.created', @@ -70,7 +70,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private workflowDeleted({ user, workflow }: AuditEventArgs['workflow-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.deleted', @@ -78,7 +78,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private workflowSaved({ user, workflow }: AuditEventArgs['workflow-saved']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.updated', @@ -159,7 +159,7 @@ export class AuditorService extends EventEmitter { * User */ - @Redacted() + @Redactable() private userDeleted(arg: AuditEventArgs['user-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.deleted', @@ -167,7 +167,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private userInvited({ user, targetUserId }: AuditEventArgs['user-invited']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.invited', @@ -175,7 +175,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private userReinvited({ user, targetUserId }: AuditEventArgs['user-reinvited']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reinvited', @@ -183,7 +183,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private userUpdated({ user, fieldsChanged }: AuditEventArgs['user-updated']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.updated', @@ -195,7 +195,7 @@ export class AuditorService extends EventEmitter { * Auth */ - @Redacted() + @Redactable() private userSignedUp(arg: AuditEventArgs['user-signed-up']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.signedup', @@ -203,7 +203,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private userLoggedIn({ user, authenticationMethod }: AuditEventArgs['user-logged-in']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.login.success', @@ -211,7 +211,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private userLoginFailed({ user, ...rest }: AuditEventArgs['user-login-failed']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.login.failed', @@ -223,8 +223,8 @@ export class AuditorService extends EventEmitter { * Click */ - @Redacted('inviter') - @Redacted('invitee') + @Redactable('inviter') + @Redactable('invitee') private userInviteEmailClick(arg: AuditEventArgs['user-invite-email-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.invitation.accepted', @@ -232,7 +232,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private userPasswordResetEmailClick(arg: AuditEventArgs['user-password-reset-email-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reset', @@ -240,7 +240,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private userPasswordResetRequestClick(arg: AuditEventArgs['user-password-reset-request-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reset.requested', @@ -252,7 +252,7 @@ export class AuditorService extends EventEmitter { * API key */ - @Redacted() + @Redactable() private apiKeyCreated(arg: AuditEventArgs['api-key-created']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.api.created', @@ -260,7 +260,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private apiKeyDeleted(arg: AuditEventArgs['api-key-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.api.deleted', @@ -272,7 +272,7 @@ export class AuditorService extends EventEmitter { * Emailing */ - @Redacted() + @Redactable() private emailFailed({ user, messageType }: AuditEventArgs['email-failed']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.email.failed', @@ -284,7 +284,7 @@ export class AuditorService extends EventEmitter { * Credentials */ - @Redacted() + @Redactable() private credentialsCreated({ user, ...rest }: AuditEventArgs['credentials-created']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.created', @@ -292,7 +292,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private credentialsDeleted({ user, ...rest }: AuditEventArgs['credentials-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.deleted', @@ -300,7 +300,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private credentialsShared({ user, ...rest }: AuditEventArgs['credentials-shared']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.shared', @@ -308,7 +308,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private credentialsUpdated({ user, ...rest }: AuditEventArgs['credentials-updated']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.updated', @@ -320,7 +320,7 @@ export class AuditorService extends EventEmitter { * Community package */ - @Redacted() + @Redactable() private communityPackageInstalled({ user, ...rest @@ -331,7 +331,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private communityPackageUpdated({ user, ...rest }: AuditEventArgs['community-package-updated']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.package.updated', @@ -339,7 +339,7 @@ export class AuditorService extends EventEmitter { }); } - @Redacted() + @Redactable() private communityPackageDeleted({ user, ...rest }: AuditEventArgs['community-package-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.package.deleted', From f17c9555ac3711f97d89f9523e32255714444665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 12:08:38 +0200 Subject: [PATCH 03/25] Better naming --- packages/cli/src/decorators/Redactable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts index e83026aba2162..d1008a96043c9 100644 --- a/packages/cli/src/decorators/Redactable.ts +++ b/packages/cli/src/decorators/Redactable.ts @@ -1,6 +1,6 @@ import type { UserLike } from '@/eventbus/audit.types'; -function markRedactable(userLike: UserLike) { +function toRedactable(userLike: UserLike) { return { userId: userLike.id, _email: userLike.email, @@ -30,7 +30,7 @@ export const Redactable = const userLike = args[index]?.[fieldName]; // @ts-expect-error Sensitive properties prepended with underscore - if (userLike) args[index][fieldName] = markRedactable(userLike); + if (userLike) args[index][fieldName] = toRedactable(userLike); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return originalMethod.apply(this, args); From b398cca30d01fb2a2f04431380d8609f092f4e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 12:13:54 +0200 Subject: [PATCH 04/25] Better docline --- packages/cli/src/decorators/Redactable.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts index d1008a96043c9..ae2480c2288c9 100644 --- a/packages/cli/src/decorators/Redactable.ts +++ b/packages/cli/src/decorators/Redactable.ts @@ -13,9 +13,9 @@ function toRedactable(userLike: UserLike) { type FieldName = 'user' | 'inviter' | 'invitee'; /** - * Mark redactable properties in a `UserLike` field in an event bus arg. - * These properties will be redacted by the log streaming destination - * if so requested by the user. + * Mark redactable properties in a `UserLike` field in an `AuditorService` + * method arg. These properties will be later redacted by the log streaming + * destination if enabled by the user. Relevant only for `n8n.audit.*` logs. */ export const Redactable = (fieldName: FieldName = 'user'): MethodDecorator => From c191cef9b07c05850105e9c193784ff7e89f29aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 13:08:47 +0200 Subject: [PATCH 05/25] Emit events --- .../credentials/credentials.service.ts | 13 +++++ .../handlers/workflows/workflows.handler.ts | 9 ++++ .../cli/src/WorkflowExecuteAdditionalData.ts | 51 ++++++++++++++++--- packages/cli/src/WorkflowRunner.ts | 11 ++++ packages/cli/src/auth/methods/ldap.ts | 2 + .../cli/src/controllers/auth.controller.ts | 14 +++-- .../communityPackages.controller.ts | 37 ++++++++++++++ .../src/controllers/invitation.controller.ts | 3 ++ packages/cli/src/controllers/me.controller.ts | 6 +++ .../controllers/passwordReset.controller.ts | 11 ++-- .../cli/src/controllers/users.controller.ts | 3 ++ .../src/credentials/credentials.controller.ts | 30 +++++++++++ packages/cli/src/eventbus/audit.types.ts | 12 ++--- packages/cli/src/eventbus/auditor.service.ts | 7 ++- .../executions/execution-recovery.service.ts | 9 ++++ packages/cli/src/services/user.service.ts | 6 +++ .../src/sso/saml/routes/saml.controller.ee.ts | 15 ++++++ .../cli/src/workflows/workflow.service.ts | 4 ++ .../cli/src/workflows/workflows.controller.ts | 3 ++ 19 files changed, 220 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts index 20b7d5f949488..58be809859e6b 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts @@ -18,6 +18,7 @@ import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { InternalHooks } from '@/InternalHooks'; +import { EventSender } from '@/eventbus/event-sender'; export async function getCredentials(credentialId: string): Promise { return await Container.get(CredentialsRepository).findOneBy({ id: credentialId }); @@ -59,6 +60,12 @@ export async function saveCredential( credential_id: credential.id, public_api: true, }); + Container.get(EventSender).emit('credentials-created', { + user, + credentialName: credential.name, + credentialType: credential.type, + credentialId: credential.id, + }); return await Db.transaction(async (transactionManager) => { const savedCredential = await transactionManager.save(credential); @@ -95,6 +102,12 @@ export async function removeCredential( credential_type: credentials.type, credential_id: credentials.id, }); + Container.get(EventSender).emit('credentials-deleted', { + user, + credentialName: credentials.name, + credentialType: credentials.type, + credentialId: credentials.id, + }); return await Container.get(CredentialsRepository).remove(credentials); } diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index 6daab565c97a1..bdee0fece73a1 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -30,6 +30,7 @@ import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflo import { TagRepository } from '@/databases/repositories/tag.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; +import { EventSender } from '@/eventbus/event-sender'; export = { createWorkflow: [ @@ -56,6 +57,10 @@ export = { await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]); void Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, project, true); + Container.get(EventSender).emit('workflow-created', { + workflow: createdWorkflow, + user: req.user, + }); return res.json(createdWorkflow); }, @@ -233,6 +238,10 @@ export = { await Container.get(ExternalHooks).run('workflow.afterUpdate', [updateData]); void Container.get(InternalHooks).onWorkflowSaved(req.user, updateData, true); + Container.get(EventSender).emit('workflow-saved', { + user: req.user, + workflow: updateData, + }); return res.json(updatedWorkflow); }, diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 5fb6a3de06c79..11bcd45483258 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -71,6 +71,7 @@ import { WorkflowRepository } from './databases/repositories/workflow.repository import { UrlService } from './services/url.service'; import { WorkflowExecutionService } from './workflows/workflowExecution.service'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; +import { EventSender } from './eventbus/event-sender'; const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); @@ -394,15 +395,22 @@ function hookFunctionsSave(): IWorkflowExecuteHooks { const logger = Container.get(Logger); const internalHooks = Container.get(InternalHooks); const eventsService = Container.get(EventsService); + const eventSender = Container.get(EventSender); return { nodeExecuteBefore: [ async function (this: WorkflowHooks, nodeName: string): Promise { - void internalHooks.onNodeBeforeExecute(this.executionId, this.workflowData, nodeName); + const { executionId, workflowData: workflow } = this; + + void internalHooks.onNodeBeforeExecute(executionId, workflow, nodeName); + eventSender.emit('node-pre-execute', { executionId, workflow, nodeName }); }, ], nodeExecuteAfter: [ async function (this: WorkflowHooks, nodeName: string): Promise { - void internalHooks.onNodePostExecute(this.executionId, this.workflowData, nodeName); + const { executionId, workflowData: workflow } = this; + + void internalHooks.onNodePostExecute(executionId, workflow, nodeName); + eventSender.emit('node-post-execute', { executionId, workflow, nodeName }); }, ], workflowExecuteBefore: [], @@ -541,20 +549,30 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { const logger = Container.get(Logger); const internalHooks = Container.get(InternalHooks); const eventsService = Container.get(EventsService); + const eventSender = Container.get(EventSender); return { nodeExecuteBefore: [ async function (this: WorkflowHooks, nodeName: string): Promise { - void internalHooks.onNodeBeforeExecute(this.executionId, this.workflowData, nodeName); + const { executionId, workflowData: workflow } = this; + + void internalHooks.onNodeBeforeExecute(executionId, workflow, nodeName); + eventSender.emit('node-pre-execute', { executionId, workflow, nodeName }); }, ], nodeExecuteAfter: [ async function (this: WorkflowHooks, nodeName: string): Promise { - void internalHooks.onNodePostExecute(this.executionId, this.workflowData, nodeName); + const { executionId, workflowData: workflow } = this; + + void internalHooks.onNodePostExecute(executionId, workflow, nodeName); + eventSender.emit('node-post-execute', { executionId, workflow, nodeName }); }, ], workflowExecuteBefore: [ async function (): Promise { - void internalHooks.onWorkflowBeforeExecute(this.executionId, this.workflowData); + const { executionId, workflowData } = this; + + void internalHooks.onWorkflowBeforeExecute(executionId, workflowData); + eventSender.emit('workflow-pre-execute', { executionId, data: workflowData }); }, ], workflowExecuteAfter: [ @@ -622,9 +640,17 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { eventsService.emit('workflowExecutionCompleted', this.workflowData, fullRunData); } }, - async function (this: WorkflowHooks, fullRunData: IRun): Promise { - // send tracking and event log events, but don't wait for them - void internalHooks.onWorkflowPostExecute(this.executionId, this.workflowData, fullRunData); + async function (this: WorkflowHooks, runData: IRun): Promise { + const { executionId, workflowData: workflow } = this; + + void internalHooks.onWorkflowPostExecute(executionId, workflow, runData); + eventSender.emit('workflow-post-execute', { + workflowId: workflow.id, + workflowName: workflow.name, + executionId, + success: runData.status === 'success', + isManual: runData.mode === 'manual', + }); }, async function (this: WorkflowHooks, fullRunData: IRun) { const externalHooks = Container.get(ExternalHooks); @@ -765,6 +791,7 @@ async function executeWorkflow( const nodeTypes = Container.get(NodeTypes); const activeExecutions = Container.get(ActiveExecutions); + const eventSender = Container.get(EventSender); const workflowData = options.loadedWorkflowData ?? @@ -905,6 +932,14 @@ async function executeWorkflow( await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]); void internalHooks.onWorkflowPostExecute(executionId, workflowData, data, additionalData.userId); + eventSender.emit('workflow-post-execute', { + workflowId: workflowData.id, + workflowName: workflowData.name, + executionId, + success: data.status === 'success', + isManual: data.mode === 'manual', + userId: additionalData.userId, + }); // subworkflow either finished, or is in status waiting due to a wait node, both cases are considered successes here if (data.finished === true || data.status === 'waiting') { diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 3ba06671c89b6..763c6bca53de1 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -37,6 +37,7 @@ import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { InternalHooks } from '@/InternalHooks'; import { Logger } from '@/Logger'; import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; +import { EventSender } from './eventbus/event-sender'; @Service() export class WorkflowRunner { @@ -52,6 +53,7 @@ export class WorkflowRunner { private readonly workflowStaticDataService: WorkflowStaticDataService, private readonly nodeTypes: NodeTypes, private readonly permissionChecker: PermissionChecker, + private readonly eventSender: EventSender, ) { if (this.executionsMode === 'queue') { this.jobQueue = Container.get(Queue); @@ -146,6 +148,7 @@ export class WorkflowRunner { } else { await this.runMainProcess(executionId, data, loadStaticData, executionId); void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId, data); + this.eventSender.emit('workflow-pre-execute', { executionId, data }); } // only run these when not in queue mode or when the execution is manual, @@ -164,6 +167,14 @@ export class WorkflowRunner { executionData, data.userId, ); + this.eventSender.emit('workflow-post-execute', { + workflowId: data.workflowData.id, + workflowName: data.workflowData.name, + executionId, + success: executionData?.status === 'success', + isManual: data.executionMode === 'manual', + userId: data.userId, + }); if (this.externalHooks.exists('workflow.postExecute')) { try { await this.externalHooks.run('workflow.postExecute', [ diff --git a/packages/cli/src/auth/methods/ldap.ts b/packages/cli/src/auth/methods/ldap.ts index 964fc9f485f56..be6fc512e5c90 100644 --- a/packages/cli/src/auth/methods/ldap.ts +++ b/packages/cli/src/auth/methods/ldap.ts @@ -12,6 +12,7 @@ import { updateLdapUserOnLocalDb, } from '@/Ldap/helpers'; import type { User } from '@db/entities/User'; +import { EventSender } from '@/eventbus/event-sender'; export const handleLdapLogin = async ( loginId: string, @@ -54,6 +55,7 @@ export const handleLdapLogin = async ( user_type: 'ldap', was_disabled_ldap_user: false, }); + Container.get(EventSender).emit('user-signed-up', { user }); return user; } } else { diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 6ba144903b9fa..6ab475872249f 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -24,6 +24,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ApplicationError } from 'n8n-workflow'; import { UserRepository } from '@/databases/repositories/user.repository'; +import { EventSender } from '@/eventbus/event-sender'; @RestController() export class AuthController { @@ -35,6 +36,7 @@ export class AuthController { private readonly userService: UserService, private readonly license: License, private readonly userRepository: UserRepository, + private readonly eventSender: EventSender, private readonly postHog?: PostHogClient, ) {} @@ -90,10 +92,9 @@ export class AuthController { } this.authService.issueCookie(res, user, req.browserId); - void this.internalHooks.onUserLoginSuccess({ - user, - authenticationMethod: usedAuthenticationMethod, - }); + const authenticationMethod = usedAuthenticationMethod; + void this.internalHooks.onUserLoginSuccess({ user, authenticationMethod }); + this.eventSender.emit('user-logged-in', { user, authenticationMethod }); return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true }); } @@ -102,6 +103,10 @@ export class AuthController { authenticationMethod: usedAuthenticationMethod, reason: 'wrong credentials', }); + this.eventSender.emit('user-login-failed', { + authenticationMethod: usedAuthenticationMethod, + userEmail: email, + }); throw new AuthError('Wrong username or password. Do you have caps lock on?'); } @@ -177,6 +182,7 @@ export class AuthController { } void this.internalHooks.onUserInviteEmailClick({ inviter, invitee }); + this.eventSender.emit('user-invite-email-click', { inviter, invitee }); const { firstName, lastName } = inviter; return { inviter: { firstName, lastName } }; diff --git a/packages/cli/src/controllers/communityPackages.controller.ts b/packages/cli/src/controllers/communityPackages.controller.ts index 681acd9d234cf..dd1821c03fd51 100644 --- a/packages/cli/src/controllers/communityPackages.controller.ts +++ b/packages/cli/src/controllers/communityPackages.controller.ts @@ -14,6 +14,7 @@ import { Push } from '@/push'; import { CommunityPackagesService } from '@/services/communityPackages.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { EventSender } from '@/eventbus/event-sender'; const { PACKAGE_NOT_INSTALLED, @@ -38,6 +39,7 @@ export class CommunityPackagesController { private readonly push: Push, private readonly internalHooks: InternalHooks, private readonly communityPackagesService: CommunityPackagesService, + private readonly eventSender: EventSender, ) {} // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` @@ -114,6 +116,14 @@ export class CommunityPackagesController { package_version: parsed.version, failure_reason: errorMessage, }); + this.eventSender.emit('community-package-installed', { + user: req.user, + packageName: parsed.packageName, + success: false, + packageVersion: parsed.version, + failureReason: errorMessage, + inputString: name, + }); let message = [`Error loading package "${name}" `, errorMessage].join(':'); if (error instanceof Error && error.cause instanceof Error) { @@ -144,6 +154,16 @@ export class CommunityPackagesController { package_author: installedPackage.authorName, package_author_email: installedPackage.authorEmail, }); + this.eventSender.emit('community-package-installed', { + user: req.user, + inputString: name, + packageName: parsed.packageName, + success: true, + packageVersion: parsed.version, + packageNodeNames: installedPackage.installedNodes.map((node) => node.name), + packageAuthor: installedPackage.authorName, + packageAuthorEmail: installedPackage.authorEmail, + }); return installedPackage; } @@ -233,6 +253,14 @@ export class CommunityPackagesController { package_author: installedPackage.authorName, package_author_email: installedPackage.authorEmail, }); + this.eventSender.emit('community-package-deleted', { + user: req.user, + packageName: name, + packageVersion: installedPackage.installedVersion, + packageNodeNames: installedPackage.installedNodes.map((node) => node.name), + packageAuthor: installedPackage.authorName, + packageAuthorEmail: installedPackage.authorEmail, + }); } @Patch('/') @@ -281,6 +309,15 @@ export class CommunityPackagesController { package_author: newInstalledPackage.authorName, package_author_email: newInstalledPackage.authorEmail, }); + this.eventSender.emit('community-package-updated', { + user: req.user, + packageName: name, + packageVersionCurrent: previouslyInstalledPackage.installedVersion, + packageVersionNew: newInstalledPackage.installedVersion, + packageNodeNames: newInstalledPackage.installedNodes.map((node) => node.name), + packageAuthor: newInstalledPackage.authorName, + packageAuthorEmail: newInstalledPackage.authorEmail, + }); return newInstalledPackage; } catch (error) { diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index 5e75db0250811..6db39c93c1ba2 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -18,6 +18,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { InternalHooks } from '@/InternalHooks'; import { ExternalHooks } from '@/ExternalHooks'; +import { EventSender } from '@/eventbus/event-sender'; @RestController('/invitations') export class InvitationController { @@ -31,6 +32,7 @@ export class InvitationController { private readonly passwordUtility: PasswordUtility, private readonly userRepository: UserRepository, private readonly postHog: PostHogClient, + private readonly eventSender: EventSender, ) {} /** @@ -170,6 +172,7 @@ export class InvitationController { user_type: 'email', was_disabled_ldap_user: false, }); + this.eventSender.emit('user-signed-up', { user: updatedUser }); const publicInvitee = await this.userService.toPublic(invitee); diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 9afe583d56eaa..a53fa35d63ecd 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -23,6 +23,7 @@ import { InternalHooks } from '@/InternalHooks'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { UserRepository } from '@/databases/repositories/user.repository'; import { isApiEnabled } from '@/PublicApi'; +import { EventSender } from '@/eventbus/event-sender'; export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => { if (isApiEnabled()) { @@ -42,6 +43,7 @@ export class MeController { private readonly userService: UserService, private readonly passwordUtility: PasswordUtility, private readonly userRepository: UserRepository, + private readonly eventSender: EventSender, ) {} /** @@ -101,6 +103,7 @@ export class MeController { user, fields_changed: updatedKeys, }); + this.eventSender.emit('user-updated', { user, fieldsChanged: updatedKeys }); const publicUser = await this.userService.toPublic(user); @@ -153,6 +156,7 @@ export class MeController { user: updatedUser, fields_changed: ['password'], }); + this.eventSender.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] }); await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]); @@ -204,6 +208,7 @@ export class MeController { user: req.user, public_api: false, }); + this.eventSender.emit('api-key-created', { user: req.user }); return { apiKey }; } @@ -227,6 +232,7 @@ export class MeController { user: req.user, public_api: false, }); + this.eventSender.emit('api-key-deleted', { user: req.user }); return { success: true }; } diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 03d19b4deca64..65af3b3096069 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -21,6 +21,7 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error'; import { UserRepository } from '@/databases/repositories/user.repository'; +import { EventSender } from '@/eventbus/event-sender'; @RestController() export class PasswordResetController { @@ -36,6 +37,7 @@ export class PasswordResetController { private readonly license: License, private readonly passwordUtility: PasswordUtility, private readonly userRepository: UserRepository, + private readonly eventSender: EventSender, ) {} /** @@ -136,6 +138,7 @@ export class PasswordResetController { }); void this.internalHooks.onUserPasswordResetRequestClick({ user }); + this.eventSender.emit('user-password-reset-request-click', { user }); } /** @@ -168,6 +171,7 @@ export class PasswordResetController { this.logger.info('Reset-password token resolved successfully', { userId: user.id }); void this.internalHooks.onUserPasswordResetEmailClick({ user }); + this.eventSender.emit('user-password-reset-email-click', { user }); } /** @@ -210,10 +214,8 @@ export class PasswordResetController { this.authService.issueCookie(res, user, req.browserId); - void this.internalHooks.onUserUpdate({ - user, - fields_changed: ['password'], - }); + void this.internalHooks.onUserUpdate({ user, fields_changed: ['password'] }); + this.eventSender.emit('user-updated', { user, fieldsChanged: ['password'] }); // if this user used to be an LDAP users const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap'); @@ -222,6 +224,7 @@ export class PasswordResetController { user_type: 'email', was_disabled_ldap_user: true, }); + this.eventSender.emit('user-signed-up', { user }); } await this.externalHooks.run('user.password.update', [user.email, passwordHash]); diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index ec7df4ff9584b..5be19a2bf7c87 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -28,6 +28,7 @@ import { Project } from '@/databases/entities/Project'; import { WorkflowService } from '@/workflows/workflow.service'; import { CredentialsService } from '@/credentials/credentials.service'; import { ProjectService } from '@/services/project.service'; +import { EventSender } from '@/eventbus/event-sender'; @RestController('/users') export class UsersController { @@ -44,6 +45,7 @@ export class UsersController { private readonly workflowService: WorkflowService, private readonly credentialsService: CredentialsService, private readonly projectService: ProjectService, + private readonly eventSender: EventSender, ) {} static ERROR_MESSAGES = { @@ -256,6 +258,7 @@ export class UsersController { telemetryData, publicApi: false, }); + this.eventSender.emit('user-deleted', { user: req.user }); await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]); diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 2dfa547a937a1..d5f65022c317f 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -29,6 +29,7 @@ import { In } from '@n8n/typeorm'; import { SharedCredentials } from '@/databases/entities/SharedCredentials'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import { z } from 'zod'; +import { EventSender } from '@/eventbus/event-sender'; @RestController('/credentials') export class CredentialsController { @@ -42,6 +43,7 @@ export class CredentialsController { private readonly userManagementMailer: UserManagementMailer, private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly projectRelationRepository: ProjectRelationRepository, + private readonly eventSender: EventSender, ) {} @Get('/', { middlewares: listQueryMiddleware }) @@ -157,6 +159,13 @@ export class CredentialsController { public_api: false, }); + this.eventSender.emit('credentials-created', { + user: req.user, + credentialName: newCredential.name, + credentialType: credential.type, + credentialId: credential.id, + }); + const scopes = await this.credentialsService.getCredentialScopes(req.user, credential.id); return { ...credential, scopes }; @@ -210,6 +219,12 @@ export class CredentialsController { credential_type: credential.type, credential_id: credential.id, }); + this.eventSender.emit('credentials-updated', { + user: req.user, + credentialName: credential.name, + credentialType: credential.type, + credentialId: credential.id, + }); const scopes = await this.credentialsService.getCredentialScopes(req.user, credential.id); @@ -245,6 +260,12 @@ export class CredentialsController { credential_type: credential.type, credential_id: credential.id, }); + this.eventSender.emit('credentials-deleted', { + user: req.user, + credentialName: credential.name, + credentialType: credential.type, + credentialId: credential.id, + }); return true; } @@ -313,6 +334,15 @@ export class CredentialsController { user_ids_sharees_added: newShareeIds, sharees_removed: amountRemoved, }); + this.eventSender.emit('credentials-shared', { + user: req.user, + credentialName: credential.name, + credentialType: credential.type, + credentialId: credential.id, + userIdSharer: req.user.id, + userIdsShareesRemoved: newShareeIds, + shareesRemoved: amountRemoved, + }); const projectsRelations = await this.projectRelationRepository.findBy({ projectId: In(newShareeIds), diff --git a/packages/cli/src/eventbus/audit.types.ts b/packages/cli/src/eventbus/audit.types.ts index 4488c0931a15f..7575fd12ca2cc 100644 --- a/packages/cli/src/eventbus/audit.types.ts +++ b/packages/cli/src/eventbus/audit.types.ts @@ -33,7 +33,7 @@ export type AuditEventArgs = { 'workflow-post-execute': { executionId: string; success: boolean; - userId: string; + userId?: string; workflowId: string; isManual: boolean; workflowName: string; @@ -81,7 +81,7 @@ export type AuditEventArgs = { }; 'user-login-failed': { - user: UserLike; + userEmail: string; authenticationMethod: AuthenticationMethod; reason?: string; }; @@ -153,10 +153,10 @@ export type AuditEventArgs = { inputString: string; packageName: string; success: boolean; - packageVersion: string; - packageNodeNames: string[]; - packageAuthor: string; - packageAuthorEmail: string; + packageVersion?: string; + packageNodeNames?: string[]; + packageAuthor?: string; + packageAuthorEmail?: string; failureReason?: string; }; diff --git a/packages/cli/src/eventbus/auditor.service.ts b/packages/cli/src/eventbus/auditor.service.ts index 0bc3792b267fd..5ba6fe543e16e 100644 --- a/packages/cli/src/eventbus/auditor.service.ts +++ b/packages/cli/src/eventbus/auditor.service.ts @@ -125,7 +125,7 @@ export class AuditorService extends EventEmitter { * Node */ - private nodePreExecute({ executionId, nodeName, workflow }: AuditEventArgs['node-pre-execute']) { + private nodePreExecute({ executionId, workflow, nodeName }: AuditEventArgs['node-pre-execute']) { void this.eventBus.sendNodeEvent({ eventName: 'n8n.node.started', payload: { @@ -211,11 +211,10 @@ export class AuditorService extends EventEmitter { }); } - @Redactable() - private userLoginFailed({ user, ...rest }: AuditEventArgs['user-login-failed']) { + private userLoginFailed(arg: AuditEventArgs['user-login-failed']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.login.failed', - payload: { ...user, ...rest }, + payload: arg, }); } diff --git a/packages/cli/src/executions/execution-recovery.service.ts b/packages/cli/src/executions/execution-recovery.service.ts index 4f6123403ad9b..7f2a01ce70c20 100644 --- a/packages/cli/src/executions/execution-recovery.service.ts +++ b/packages/cli/src/executions/execution-recovery.service.ts @@ -11,6 +11,7 @@ import type { IExecutionResponse } from '@/Interfaces'; import { NodeCrashedError } from '@/errors/node-crashed.error'; import { WorkflowCrashedError } from '@/errors/workflow-crashed.error'; import { ARTIFICIAL_TASK_DATA } from '@/constants'; +import { EventSender } from '@/eventbus/event-sender'; /** * Service for recovering key properties in executions. @@ -20,6 +21,7 @@ export class ExecutionRecoveryService { constructor( private readonly push: Push, private readonly executionRepository: ExecutionRepository, + private readonly eventSender: EventSender, ) {} /** @@ -169,6 +171,13 @@ export class ExecutionRecoveryService { stoppedAt: execution.stoppedAt, status: execution.status, }); + this.eventSender.emit('workflow-post-execute', { + workflowId: execution.workflowData.id, + workflowName: execution.workflowData.name, + executionId: execution.id, + success: execution.status === 'success', + isManual: execution.mode === 'manual', + }); const externalHooks = getWorkflowHooksMain( { diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index e9681d68da818..3c403cf33cfb2 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -12,6 +12,7 @@ import { InternalHooks } from '@/InternalHooks'; import { UrlService } from '@/services/url.service'; import type { UserRequest } from '@/requests'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { EventSender } from '@/eventbus/event-sender'; @Service() export class UserService { @@ -20,6 +21,7 @@ export class UserService { private readonly userRepository: UserRepository, private readonly mailer: UserManagementMailer, private readonly urlService: UrlService, + private readonly eventSender: EventSender, ) {} async update(userId: string, data: Partial) { @@ -156,6 +158,10 @@ export class UserService { email_sent: result.emailSent, invitee_role: role, // same role for all invited users }); + this.eventSender.emit('user-invited', { + user: owner, + targetUserId: Object.values(toInviteUsers), + }); } catch (e) { if (e instanceof Error) { void Container.get(InternalHooks).onEmailFailed({ diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts index 38de44c230b70..459ab374eaa81 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts @@ -28,6 +28,7 @@ import { import { SamlService } from '../saml.service.ee'; import { SamlConfiguration } from '../types/requests'; import { getInitSSOFormView } from '../views/initSsoPost'; +import { EventSender } from '@/eventbus/event-sender'; @RestController('/sso/saml') export class SamlController { @@ -36,6 +37,7 @@ export class SamlController { private readonly samlService: SamlService, private readonly urlService: UrlService, private readonly internalHooks: InternalHooks, + private readonly eventSender: EventSender, ) {} @Get('/metadata', { skipAuth: true }) @@ -130,6 +132,11 @@ export class SamlController { user: loginResult.authenticatedUser, authenticationMethod: 'saml', }); + this.eventSender.emit('user-logged-in', { + user: loginResult.authenticatedUser, + authenticationMethod: 'saml', + }); + // Only sign in user if SAML is enabled, otherwise treat as test connection if (isSamlLicensedAndEnabled()) { this.authService.issueCookie(res, loginResult.authenticatedUser, req.browserId); @@ -147,6 +154,10 @@ export class SamlController { user: loginResult.attributes.email ?? 'unknown', authenticationMethod: 'saml', }); + this.eventSender.emit('user-login-failed', { + userEmail: loginResult.attributes.email ?? 'unknown', + authenticationMethod: 'saml', + }); throw new AuthError('SAML Authentication failed'); } catch (error) { if (isConnectionTestRequest(req)) { @@ -156,6 +167,10 @@ export class SamlController { user: 'unknown', authenticationMethod: 'saml', }); + this.eventSender.emit('user-login-failed', { + userEmail: 'unknown', + authenticationMethod: 'saml', + }); throw new AuthError('SAML Authentication failed: ' + (error as Error).message); } } diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index d03fd65646378..4742e4bd2f186 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -32,6 +32,7 @@ import type { Scope } from '@n8n/permissions'; import type { EntityManager } from '@n8n/typeorm'; import { In } from '@n8n/typeorm'; import { SharedWorkflow } from '@/databases/entities/SharedWorkflow'; +import { EventSender } from '@/eventbus/event-sender'; @Service() export class WorkflowService { @@ -51,6 +52,7 @@ export class WorkflowService { private readonly workflowSharingService: WorkflowSharingService, private readonly projectService: ProjectService, private readonly executionRepository: ExecutionRepository, + private readonly eventSender: EventSender, ) {} async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) { @@ -216,6 +218,7 @@ export class WorkflowService { await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]); void Container.get(InternalHooks).onWorkflowSaved(user, updatedWorkflow, false); + this.eventSender.emit('workflow-saved', { user, workflow: updatedWorkflow }); if (updatedWorkflow.active) { // When the workflow is supposed to be active add it again @@ -274,6 +277,7 @@ export class WorkflowService { await this.binaryDataService.deleteMany(idsForDeletion); void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false); + this.eventSender.emit('workflow-deleted', { user, workflow }); await this.externalHooks.run('workflow.afterDelete', [workflowId]); return workflow; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index a33194a31e545..282a95cde3472 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -41,6 +41,7 @@ import { In, type FindOptionsRelations } from '@n8n/typeorm'; import type { Project } from '@/databases/entities/Project'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import { z } from 'zod'; +import { EventSender } from '@/eventbus/event-sender'; @RestController('/workflows') export class WorkflowsController { @@ -64,6 +65,7 @@ export class WorkflowsController { private readonly projectRepository: ProjectRepository, private readonly projectService: ProjectService, private readonly projectRelationRepository: ProjectRelationRepository, + private readonly eventSender: EventSender, ) {} @Post('/') @@ -175,6 +177,7 @@ export class WorkflowsController { await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]); void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false); + this.eventSender.emit('workflow-created', { user: req.user, workflow: savedWorkflow }); const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id); From c8f4257ccaab07a4fbd52fdc1ce45789ff1068ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 13:29:02 +0200 Subject: [PATCH 06/25] Remove event bus from internal hooks --- packages/cli/src/InternalHooks.ts | 450 ++---------------- .../cli/src/WorkflowExecuteAdditionalData.ts | 8 +- packages/cli/src/WorkflowRunner.ts | 1 - .../cli/src/controllers/auth.controller.ts | 6 - .../src/sso/saml/routes/saml.controller.ee.ts | 12 - 5 files changed, 43 insertions(+), 434 deletions(-) diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index f0c063c2a6e7b..ac45326d4d2e7 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -3,13 +3,11 @@ import { snakeCase } from 'change-case'; import os from 'node:os'; import { get as pslGet } from 'psl'; import type { - AuthenticationMethod, ExecutionStatus, INodesGraphResult, IRun, ITelemetryTrackProperties, IWorkflowBase, - WorkflowExecuteMode, } from 'n8n-workflow'; import { TelemetryHelpers } from 'n8n-workflow'; import { InstanceSettings } from 'n8n-core'; @@ -18,17 +16,13 @@ import config from '@/config'; import { N8N_VERSION } from '@/constants'; import type { AuthProviderType } from '@db/entities/AuthIdentity'; import type { GlobalRole, User } from '@db/entities/User'; -import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import type { EventPayloadWorkflow } from '@/eventbus'; -import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions'; import type { ITelemetryUserDeletionData, IWorkflowDb, IExecutionTrackProperties, - IWorkflowExecutionDataProcess, } from '@/Interfaces'; import { License } from '@/License'; import { EventsService } from '@/services/events.service'; @@ -39,22 +33,6 @@ import type { ProjectRole } from '@db/entities/ProjectRelation'; import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository'; import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository'; -function userToPayload(user: User): { - userId: string; - _email: string; - _firstName: string; - _lastName: string; - globalRole: GlobalRole; -} { - return { - userId: user.id, - _email: user.email, - _firstName: user.firstName, - _lastName: user.lastName, - globalRole: user.role, - }; -} - @Service() export class InternalHooks { constructor( @@ -64,7 +42,6 @@ export class InternalHooks { private readonly workflowRepository: WorkflowRepository, eventsService: EventsService, private readonly instanceSettings: InstanceSettings, - private readonly eventBus: MessageEventBus, private readonly license: License, private readonly projectRelationRepository: ProjectRelationRepository, private readonly sharedCredentialsRepository: SharedCredentialsRepository, @@ -177,41 +154,23 @@ export class InternalHooks { publicApi: boolean, ): Promise { const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes); - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.workflow.created', - payload: { - ...userToPayload(user), - workflowId: workflow.id, - workflowName: workflow.name, - }, - }), - this.telemetry.track('User created workflow', { - user_id: user.id, - workflow_id: workflow.id, - node_graph_string: JSON.stringify(nodeGraph), - public_api: publicApi, - project_id: project.id, - project_type: project.type, - }), - ]); + + void this.telemetry.track('User created workflow', { + user_id: user.id, + workflow_id: workflow.id, + node_graph_string: JSON.stringify(nodeGraph), + public_api: publicApi, + project_id: project.id, + project_type: project.type, + }); } async onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise { - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.workflow.deleted', - payload: { - ...userToPayload(user), - workflowId, - }, - }), - this.telemetry.track('User deleted workflow', { - user_id: user.id, - workflow_id: workflowId, - public_api: publicApi, - }), - ]); + void this.telemetry.track('User deleted workflow', { + user_id: user.id, + workflow_id: workflowId, + public_api: publicApi, + }); } async onWorkflowSaved(user: User, workflow: IWorkflowDb, publicApi: boolean): Promise { @@ -247,127 +206,22 @@ export class InternalHooks { (note) => note.overlapping, ).length; - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.workflow.updated', - payload: { - ...userToPayload(user), - workflowId: workflow.id, - workflowName: workflow.name, - }, - }), - this.telemetry.track('User saved workflow', { - user_id: user.id, - workflow_id: workflow.id, - node_graph_string: JSON.stringify(nodeGraph), - notes_count_overlapping: overlappingCount, - notes_count_non_overlapping: notesCount - overlappingCount, - version_cli: N8N_VERSION, - num_tags: workflow.tags?.length ?? 0, - public_api: publicApi, - sharing_role: userRole, - }), - ]); - } - - async onNodeBeforeExecute( - executionId: string, - workflow: IWorkflowBase, - nodeName: string, - ): Promise { - const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName); - void this.eventBus.sendNodeEvent({ - eventName: 'n8n.node.started', - payload: { - executionId, - nodeName, - workflowId: workflow.id?.toString(), - workflowName: workflow.name, - nodeType: nodeInWorkflow?.type, - }, - }); - } - - async onNodePostExecute( - executionId: string, - workflow: IWorkflowBase, - nodeName: string, - ): Promise { - const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName); - void this.eventBus.sendNodeEvent({ - eventName: 'n8n.node.finished', - payload: { - executionId, - nodeName, - workflowId: workflow.id?.toString(), - workflowName: workflow.name, - nodeType: nodeInWorkflow?.type, - }, - }); - } - - async onWorkflowBeforeExecute( - executionId: string, - data: IWorkflowExecutionDataProcess | IWorkflowBase, - ): Promise { - let payload: EventPayloadWorkflow; - // this hook is called slightly differently depending on whether it's from a worker or the main instance - // in the worker context, meaning in queue mode, only IWorkflowBase is available - if ('executionData' in data) { - payload = { - executionId, - userId: data.userId ?? undefined, - workflowId: data.workflowData.id?.toString(), - isManual: data.executionMode === 'manual', - workflowName: data.workflowData.name, - }; - } else { - payload = { - executionId, - userId: undefined, - workflowId: (data as IWorkflowBase).id?.toString(), - isManual: false, - workflowName: (data as IWorkflowBase).name, - }; - } - void this.eventBus.sendWorkflowEvent({ - eventName: 'n8n.workflow.started', - payload, + void this.telemetry.track('User saved workflow', { + user_id: user.id, + workflow_id: workflow.id, + node_graph_string: JSON.stringify(nodeGraph), + notes_count_overlapping: overlappingCount, + notes_count_non_overlapping: notesCount - overlappingCount, + version_cli: N8N_VERSION, + num_tags: workflow.tags?.length ?? 0, + public_api: publicApi, + sharing_role: userRole, }); } - async onWorkflowCrashed( - executionId: string, - executionMode: WorkflowExecuteMode, - workflowData?: IWorkflowBase, - executionMetadata?: ExecutionMetadata[], - ): Promise { - let metaData; - try { - if (executionMetadata) { - metaData = executionMetadata.reduce((acc, meta) => { - return { ...acc, [meta.key]: meta.value }; - }, {}); - } - } catch {} - - void Promise.all([ - this.eventBus.sendWorkflowEvent({ - eventName: 'n8n.workflow.crashed', - payload: { - executionId, - isManual: executionMode === 'manual', - workflowId: workflowData?.id?.toString(), - workflowName: workflowData?.name, - metaData, - }, - }), - ]); - } - // eslint-disable-next-line complexity async onWorkflowPostExecute( - executionId: string, + _executionId: string, workflow: IWorkflowBase, runData?: IRun, userId?: string, @@ -505,36 +359,6 @@ export class InternalHooks { } } - const sharedEventPayload: EventPayloadWorkflow = { - executionId, - success: telemetryProperties.success, - userId: telemetryProperties.user_id, - workflowId: workflow.id, - isManual: telemetryProperties.is_manual, - workflowName: workflow.name, - metaData: runData?.data?.resultData?.metadata, - }; - let event; - if (telemetryProperties.success) { - event = this.eventBus.sendWorkflowEvent({ - eventName: 'n8n.workflow.success', - payload: sharedEventPayload, - }); - } else { - event = this.eventBus.sendWorkflowEvent({ - eventName: 'n8n.workflow.failed', - payload: { - ...sharedEventPayload, - lastNodeExecuted: runData?.data.resultData.lastNodeExecuted, - errorNodeType: telemetryProperties.error_node_type, - errorNodeId: telemetryProperties.error_node_id?.toString(), - errorMessage: telemetryProperties.error_message?.toString(), - }, - }); - } - - promises.push(event); - void Promise.all([...promises, this.telemetry.trackWorkflowExecution(telemetryProperties)]); } @@ -563,19 +387,11 @@ export class InternalHooks { telemetryData: ITelemetryUserDeletionData; publicApi: boolean; }): Promise { - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.deleted', - payload: { - ...userToPayload(userDeletionData.user), - }, - }), - this.telemetry.track('User deleted user', { - ...userDeletionData.telemetryData, - user_id: userDeletionData.user.id, - public_api: userDeletionData.publicApi, - }), - ]); + void this.telemetry.track('User deleted user', { + ...userDeletionData.telemetryData, + user_id: userDeletionData.user.id, + public_api: userDeletionData.publicApi, + }); } async onUserInvite(userInviteData: { @@ -585,23 +401,13 @@ export class InternalHooks { email_sent: boolean; invitee_role: string; }): Promise { - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.invited', - payload: { - ...userToPayload(userInviteData.user), - targetUserId: userInviteData.target_user_id, - }, - }), - - this.telemetry.track('User invited new user', { - user_id: userInviteData.user.id, - target_user_id: userInviteData.target_user_id, - public_api: userInviteData.public_api, - email_sent: userInviteData.email_sent, - invitee_role: userInviteData.invitee_role, - }), - ]); + void this.telemetry.track('User invited new user', { + user_id: userInviteData.user.id, + target_user_id: userInviteData.target_user_id, + public_api: userInviteData.public_api, + email_sent: userInviteData.email_sent, + invitee_role: userInviteData.invitee_role, + }); } async onUserRoleChange(userRoleChangeData: { @@ -620,20 +426,11 @@ export class InternalHooks { target_user_id: string; public_api: boolean; }): Promise { - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.reinvited', - payload: { - ...userToPayload(userReinviteData.user), - targetUserId: userReinviteData.target_user_id, - }, - }), - this.telemetry.track('User resent new user invite email', { - user_id: userReinviteData.user.id, - target_user_id: userReinviteData.target_user_id, - public_api: userReinviteData.public_api, - }), - ]); + void this.telemetry.track('User resent new user invite email', { + user_id: userReinviteData.user.id, + target_user_id: userReinviteData.target_user_id, + public_api: userReinviteData.public_api, + }); } async onUserRetrievedUser(userRetrievedData: { @@ -680,13 +477,6 @@ export class InternalHooks { async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.updated', - payload: { - ...userToPayload(userUpdateData.user), - fieldsChanged: userUpdateData.fields_changed, - }, - }), this.telemetry.track('User changed personal settings', { user_id: userUpdateData.user.id, fields_changed: userUpdateData.fields_changed, @@ -699,17 +489,6 @@ export class InternalHooks { invitee: User; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.invitation.accepted', - payload: { - invitee: { - ...userToPayload(userInviteClickData.invitee), - }, - inviter: { - ...userToPayload(userInviteClickData.inviter), - }, - }, - }), this.telemetry.track('User clicked invite link from email', { user_id: userInviteClickData.invitee.id, }), @@ -718,12 +497,6 @@ export class InternalHooks { async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.reset', - payload: { - ...userToPayload(userPasswordResetData.user), - }, - }), this.telemetry.track('User clicked password reset link from email', { user_id: userPasswordResetData.user.id, }), @@ -757,12 +530,6 @@ export class InternalHooks { async onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.api.deleted', - payload: { - ...userToPayload(apiKeyDeletedData.user), - }, - }), this.telemetry.track('API key deleted', { user_id: apiKeyDeletedData.user.id, public_api: apiKeyDeletedData.public_api, @@ -772,12 +539,6 @@ export class InternalHooks { async onApiKeyCreated(apiKeyCreatedData: { user: User; public_api: boolean }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.api.created', - payload: { - ...userToPayload(apiKeyCreatedData.user), - }, - }), this.telemetry.track('API key created', { user_id: apiKeyCreatedData.user.id, public_api: apiKeyCreatedData.public_api, @@ -787,12 +548,6 @@ export class InternalHooks { async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.reset.requested', - payload: { - ...userToPayload(userPasswordResetData.user), - }, - }), this.telemetry.track('User requested password reset while logged out', { user_id: userPasswordResetData.user.id, }), @@ -811,12 +566,6 @@ export class InternalHooks { }, ): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.signedup', - payload: { - ...userToPayload(user), - }, - }), this.telemetry.track('User signed up', { user_id: user.id, ...userSignupData, @@ -835,51 +584,12 @@ export class InternalHooks { public_api: boolean; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.email.failed', - payload: { - messageType: failedEmailData.message_type, - ...userToPayload(failedEmailData.user), - }, - }), this.telemetry.track('Instance failed to send transactional email to user', { user_id: failedEmailData.user.id, }), ]); } - async onUserLoginSuccess(userLoginData: { - user: User; - authenticationMethod: AuthenticationMethod; - }): Promise { - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.login.success', - payload: { - authenticationMethod: userLoginData.authenticationMethod, - ...userToPayload(userLoginData.user), - }, - }), - ]); - } - - async onUserLoginFailed(userLoginData: { - user: string; - authenticationMethod: AuthenticationMethod; - reason?: string; - }): Promise { - void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.login.failed', - payload: { - authenticationMethod: userLoginData.authenticationMethod, - user: userLoginData.user, - reason: userLoginData.reason, - }, - }), - ]); - } - /** * Credentials */ @@ -895,15 +605,6 @@ export class InternalHooks { userCreatedCredentialsData.credential_id, ); void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.credentials.created', - payload: { - ...userToPayload(userCreatedCredentialsData.user), - credentialName: userCreatedCredentialsData.credential_name, - credentialType: userCreatedCredentialsData.credential_type, - credentialId: userCreatedCredentialsData.credential_id, - }, - }), this.telemetry.track('User created credentials', { user_id: userCreatedCredentialsData.user.id, credential_type: userCreatedCredentialsData.credential_type, @@ -925,18 +626,6 @@ export class InternalHooks { sharees_removed: number | null; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.credentials.shared', - payload: { - ...userToPayload(userSharedCredentialsData.user), - credentialName: userSharedCredentialsData.credential_name, - credentialType: userSharedCredentialsData.credential_type, - credentialId: userSharedCredentialsData.credential_id, - userIdSharer: userSharedCredentialsData.user_id_sharer, - userIdsShareesAdded: userSharedCredentialsData.user_ids_sharees_added, - shareesRemoved: userSharedCredentialsData.sharees_removed, - }, - }), this.telemetry.track('User updated cred sharing', { user_id: userSharedCredentialsData.user.id, credential_type: userSharedCredentialsData.credential_type, @@ -956,15 +645,6 @@ export class InternalHooks { credential_id: string; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.credentials.updated', - payload: { - ...userToPayload(userUpdatedCredentialsData.user), - credentialName: userUpdatedCredentialsData.credential_name, - credentialType: userUpdatedCredentialsData.credential_type, - credentialId: userUpdatedCredentialsData.credential_id, - }, - }), this.telemetry.track('User updated credentials', { user_id: userUpdatedCredentialsData.user.id, credential_type: userUpdatedCredentialsData.credential_type, @@ -980,15 +660,6 @@ export class InternalHooks { credential_id: string; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.user.credentials.deleted', - payload: { - ...userToPayload(userUpdatedCredentialsData.user), - credentialName: userUpdatedCredentialsData.credential_name, - credentialType: userUpdatedCredentialsData.credential_type, - credentialId: userUpdatedCredentialsData.credential_id, - }, - }), this.telemetry.track('User deleted credentials', { user_id: userUpdatedCredentialsData.user.id, credential_type: userUpdatedCredentialsData.credential_type, @@ -1014,20 +685,6 @@ export class InternalHooks { failure_reason?: string; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.package.installed', - payload: { - ...userToPayload(installationData.user), - inputString: installationData.input_string, - packageName: installationData.package_name, - success: installationData.success, - packageVersion: installationData.package_version, - packageNodeNames: installationData.package_node_names, - packageAuthor: installationData.package_author, - packageAuthorEmail: installationData.package_author_email, - failureReason: installationData.failure_reason, - }, - }), this.telemetry.track('cnr package install finished', { user_id: installationData.user.id, input_string: installationData.input_string, @@ -1052,18 +709,6 @@ export class InternalHooks { package_author_email?: string; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.package.updated', - payload: { - ...userToPayload(updateData.user), - packageName: updateData.package_name, - packageVersionCurrent: updateData.package_version_current, - packageVersionNew: updateData.package_version_new, - packageNodeNames: updateData.package_node_names, - packageAuthor: updateData.package_author, - packageAuthorEmail: updateData.package_author_email, - }, - }), this.telemetry.track('cnr package updated', { user_id: updateData.user.id, package_name: updateData.package_name, @@ -1085,17 +730,6 @@ export class InternalHooks { package_author_email?: string; }): Promise { void Promise.all([ - this.eventBus.sendAuditEvent({ - eventName: 'n8n.audit.package.deleted', - payload: { - ...userToPayload(deleteData.user), - packageName: deleteData.package_name, - packageVersion: deleteData.package_version, - packageNodeNames: deleteData.package_node_names, - packageAuthor: deleteData.package_author, - packageAuthorEmail: deleteData.package_author_email, - }, - }), this.telemetry.track('cnr package deleted', { user_id: deleteData.user.id, package_name: deleteData.package_name, diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 11bcd45483258..fe4ca20a04d3c 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -393,7 +393,6 @@ export function hookFunctionsPreExecute(): IWorkflowExecuteHooks { */ function hookFunctionsSave(): IWorkflowExecuteHooks { const logger = Container.get(Logger); - const internalHooks = Container.get(InternalHooks); const eventsService = Container.get(EventsService); const eventSender = Container.get(EventSender); return { @@ -401,7 +400,6 @@ function hookFunctionsSave(): IWorkflowExecuteHooks { async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - void internalHooks.onNodeBeforeExecute(executionId, workflow, nodeName); eventSender.emit('node-pre-execute', { executionId, workflow, nodeName }); }, ], @@ -409,7 +407,6 @@ function hookFunctionsSave(): IWorkflowExecuteHooks { async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - void internalHooks.onNodePostExecute(executionId, workflow, nodeName); eventSender.emit('node-post-execute', { executionId, workflow, nodeName }); }, ], @@ -555,7 +552,6 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - void internalHooks.onNodeBeforeExecute(executionId, workflow, nodeName); eventSender.emit('node-pre-execute', { executionId, workflow, nodeName }); }, ], @@ -563,7 +559,6 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - void internalHooks.onNodePostExecute(executionId, workflow, nodeName); eventSender.emit('node-post-execute', { executionId, workflow, nodeName }); }, ], @@ -571,7 +566,6 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { async function (): Promise { const { executionId, workflowData } = this; - void internalHooks.onWorkflowBeforeExecute(executionId, workflowData); eventSender.emit('workflow-pre-execute', { executionId, data: workflowData }); }, ], @@ -819,7 +813,7 @@ async function executeWorkflow( executionId = options.parentExecutionId ?? (await activeExecutions.add(runData)); } - void internalHooks.onWorkflowBeforeExecute(executionId || '', runData); + Container.get(EventSender).emit('workflow-pre-execute', { executionId, data: runData }); let data; try { diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 763c6bca53de1..30b0d5420e5be 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -147,7 +147,6 @@ export class WorkflowRunner { await this.enqueueExecution(executionId, data, loadStaticData, realtime); } else { await this.runMainProcess(executionId, data, loadStaticData, executionId); - void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId, data); this.eventSender.emit('workflow-pre-execute', { executionId, data }); } diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 6ab475872249f..c4d526d4c3d03 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -93,16 +93,10 @@ export class AuthController { this.authService.issueCookie(res, user, req.browserId); const authenticationMethod = usedAuthenticationMethod; - void this.internalHooks.onUserLoginSuccess({ user, authenticationMethod }); this.eventSender.emit('user-logged-in', { user, authenticationMethod }); return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true }); } - void this.internalHooks.onUserLoginFailed({ - user: email, - authenticationMethod: usedAuthenticationMethod, - reason: 'wrong credentials', - }); this.eventSender.emit('user-login-failed', { authenticationMethod: usedAuthenticationMethod, userEmail: email, diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts index 459ab374eaa81..43a745b9df7da 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts @@ -128,10 +128,6 @@ export class SamlController { } } if (loginResult.authenticatedUser) { - void this.internalHooks.onUserLoginSuccess({ - user: loginResult.authenticatedUser, - authenticationMethod: 'saml', - }); this.eventSender.emit('user-logged-in', { user: loginResult.authenticatedUser, authenticationMethod: 'saml', @@ -150,10 +146,6 @@ export class SamlController { return res.status(202).send(loginResult.attributes); } } - void this.internalHooks.onUserLoginFailed({ - user: loginResult.attributes.email ?? 'unknown', - authenticationMethod: 'saml', - }); this.eventSender.emit('user-login-failed', { userEmail: loginResult.attributes.email ?? 'unknown', authenticationMethod: 'saml', @@ -163,10 +155,6 @@ export class SamlController { if (isConnectionTestRequest(req)) { return res.send(getSamlConnectionTestFailedView((error as Error).message)); } - void this.internalHooks.onUserLoginFailed({ - user: 'unknown', - authenticationMethod: 'saml', - }); this.eventSender.emit('user-login-failed', { userEmail: 'unknown', authenticationMethod: 'saml', From 7790e5f127ba3dadac70ff18a941b784097eb3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 13:46:50 +0200 Subject: [PATCH 07/25] Fix tests --- .../__tests__/auditor.service.test.ts | 58 ++++++++++++++++--- packages/cli/src/eventbus/auditor.service.ts | 30 +++++----- 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/eventbus/__tests__/auditor.service.test.ts b/packages/cli/src/eventbus/__tests__/auditor.service.test.ts index 6d7d765a66142..3cecd258981dc 100644 --- a/packages/cli/src/eventbus/__tests__/auditor.service.test.ts +++ b/packages/cli/src/eventbus/__tests__/auditor.service.test.ts @@ -7,11 +7,15 @@ describe('AuditorService', () => { const eventBus = mock(); const auditor = new AuditorService(eventBus); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should handle `user-deleted` event', () => { const arg: AuditEventArgs['user-deleted'] = { user: { id: '123', - email: 'test@example.com', + email: 'john@n8n.io', firstName: 'John', lastName: 'Doe', role: 'some-role', @@ -24,12 +28,52 @@ describe('AuditorService', () => { expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ eventName: 'n8n.audit.user.deleted', payload: { - user: { - id: arg.user.id, - email: undefined, - firstName: undefined, - lastName: undefined, - role: arg.user.role, + userId: '123', + _email: 'john@n8n.io', + _firstName: 'John', + _lastName: 'Doe', + globalRole: 'some-role', + }, + }); + }); + + it('should handle `user-invite-email-click` event', () => { + const arg: AuditEventArgs['user-invite-email-click'] = { + inviter: { + id: '123', + email: 'john@n8n.io', + firstName: 'John', + lastName: 'Doe', + role: 'some-role', + }, + invitee: { + id: '456', + email: 'jane@n8n.io', + firstName: 'Jane', + lastName: 'Doe', + role: 'some-other-role', + }, + }; + + // @ts-expect-error Private method + auditor.userInviteEmailClick(arg); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.invitation.accepted', + payload: { + inviter: { + userId: '123', + _email: 'john@n8n.io', + _firstName: 'John', + _lastName: 'Doe', + globalRole: 'some-role', + }, + invitee: { + userId: '456', + _email: 'jane@n8n.io', + _firstName: 'Jane', + _lastName: 'Doe', + globalRole: 'some-other-role', }, }, }); diff --git a/packages/cli/src/eventbus/auditor.service.ts b/packages/cli/src/eventbus/auditor.service.ts index 5ba6fe543e16e..26c7d335c057a 100644 --- a/packages/cli/src/eventbus/auditor.service.ts +++ b/packages/cli/src/eventbus/auditor.service.ts @@ -160,10 +160,10 @@ export class AuditorService extends EventEmitter { */ @Redactable() - private userDeleted(arg: AuditEventArgs['user-deleted']) { + private userDeleted({ user }: AuditEventArgs['user-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.deleted', - payload: arg, + payload: user, }); } @@ -196,10 +196,10 @@ export class AuditorService extends EventEmitter { */ @Redactable() - private userSignedUp(arg: AuditEventArgs['user-signed-up']) { + private userSignedUp({ user }: AuditEventArgs['user-signed-up']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.signedup', - payload: arg, + payload: user, }); } @@ -211,7 +211,9 @@ export class AuditorService extends EventEmitter { }); } - private userLoginFailed(arg: AuditEventArgs['user-login-failed']) { + private userLoginFailed( + arg: AuditEventArgs['user-login-failed'] /* exception: no `UserLike` to redact */, + ) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.login.failed', payload: arg, @@ -232,18 +234,20 @@ export class AuditorService extends EventEmitter { } @Redactable() - private userPasswordResetEmailClick(arg: AuditEventArgs['user-password-reset-email-click']) { + private userPasswordResetEmailClick({ user }: AuditEventArgs['user-password-reset-email-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reset', - payload: arg, + payload: user, }); } @Redactable() - private userPasswordResetRequestClick(arg: AuditEventArgs['user-password-reset-request-click']) { + private userPasswordResetRequestClick({ + user, + }: AuditEventArgs['user-password-reset-request-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reset.requested', - payload: arg, + payload: user, }); } @@ -252,18 +256,18 @@ export class AuditorService extends EventEmitter { */ @Redactable() - private apiKeyCreated(arg: AuditEventArgs['api-key-created']) { + private apiKeyCreated({ user }: AuditEventArgs['api-key-created']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.api.created', - payload: arg, + payload: user, }); } @Redactable() - private apiKeyDeleted(arg: AuditEventArgs['api-key-deleted']) { + private apiKeyDeleted({ user }: AuditEventArgs['api-key-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.api.deleted', - payload: arg, + payload: user, }); } From b27d9173a60f562ae3879aaf7116c1b025b57ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 13:49:39 +0200 Subject: [PATCH 08/25] Clarify docline --- packages/cli/src/decorators/Redactable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts index ae2480c2288c9..ed0d2374f627f 100644 --- a/packages/cli/src/decorators/Redactable.ts +++ b/packages/cli/src/decorators/Redactable.ts @@ -15,7 +15,7 @@ type FieldName = 'user' | 'inviter' | 'invitee'; /** * Mark redactable properties in a `UserLike` field in an `AuditorService` * method arg. These properties will be later redacted by the log streaming - * destination if enabled by the user. Relevant only for `n8n.audit.*` logs. + * destination based on user prefs. Relevant only for `n8n.audit.*` logs. */ export const Redactable = (fieldName: FieldName = 'user'): MethodDecorator => From 0b1c9860084d6112d3dbe969a63e230f4360919d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 13:50:19 +0200 Subject: [PATCH 09/25] Remove outdated comments --- packages/cli/src/eventbus/auditor.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cli/src/eventbus/auditor.service.ts b/packages/cli/src/eventbus/auditor.service.ts index 26c7d335c057a..38762fafe8a8c 100644 --- a/packages/cli/src/eventbus/auditor.service.ts +++ b/packages/cli/src/eventbus/auditor.service.ts @@ -5,9 +5,6 @@ import { Redactable } from '@/decorators/Redactable'; import type { AuditEventArgs } from './audit.types'; import type { IWorkflowBase } from 'n8n-workflow'; -// @TODO: Emit all these events -// @TODO: Remove eventbus from InternalHooks - /** * Service for adding events from `EventSender` to the audit log. */ From d2d0b4caf9f502c1e6f4877e755bf040aec4a8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 13:59:11 +0200 Subject: [PATCH 10/25] Remove unused hook --- packages/cli/src/InternalHooks.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index ac45326d4d2e7..c4d091213ec11 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -421,18 +421,6 @@ export class InternalHooks { void this.telemetry.track('User changed role', { user_id: user.id, ...rest }); } - async onUserReinvite(userReinviteData: { - user: User; - target_user_id: string; - public_api: boolean; - }): Promise { - void this.telemetry.track('User resent new user invite email', { - user_id: userReinviteData.user.id, - target_user_id: userReinviteData.target_user_id, - public_api: userReinviteData.public_api, - }); - } - async onUserRetrievedUser(userRetrievedData: { user_id: string; public_api: boolean; From 74316c455ba0d21f9bd585d16a78a05a838362d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 14:05:13 +0200 Subject: [PATCH 11/25] Simplify --- packages/cli/src/controllers/auth.controller.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index c4d526d4c3d03..4cbcaa5fe264b 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -92,8 +92,11 @@ export class AuthController { } this.authService.issueCookie(res, user, req.browserId); - const authenticationMethod = usedAuthenticationMethod; - this.eventSender.emit('user-logged-in', { user, authenticationMethod }); + + this.eventSender.emit('user-logged-in', { + user, + authenticationMethod: usedAuthenticationMethod, + }); return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true }); } From 408e21ff3e5093b1c9ba7b2680fced5e9f003d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 14:05:37 +0200 Subject: [PATCH 12/25] Clarify --- packages/cli/src/decorators/Redactable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts index ed0d2374f627f..117ea8502891c 100644 --- a/packages/cli/src/decorators/Redactable.ts +++ b/packages/cli/src/decorators/Redactable.ts @@ -29,7 +29,7 @@ export const Redactable = const userLike = args[index]?.[fieldName]; - // @ts-expect-error Sensitive properties prepended with underscore + // @ts-expect-error Transformation if (userLike) args[index][fieldName] = toRedactable(userLike); // eslint-disable-next-line @typescript-eslint/no-unsafe-return From dc40e689903a975eeb9c9dd5f429cc8b331d94bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 14:20:07 +0200 Subject: [PATCH 13/25] Fix saml tests --- .../test/integration/saml/saml.api.test.ts | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index b9dbe7cb0fa90..2b26f94119efd 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -1,10 +1,8 @@ import { Container } from 'typedi'; -import type { AuthenticationMethod } from 'n8n-workflow'; import type { User } from '@db/entities/User'; import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; -import { InternalHooks } from '@/InternalHooks'; import { SamlService } from '@/sso/saml/saml.service.ee'; import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes'; @@ -294,19 +292,8 @@ describe('SAML login flow', () => { }, ); - const mockedHookOnUserLoginSuccess = jest.spyOn( - Container.get(InternalHooks), - 'onUserLoginSuccess', - ); - mockedHookOnUserLoginSuccess.mockImplementation( - async (userLoginData: { user: User; authenticationMethod: AuthenticationMethod }) => { - expect(userLoginData.authenticationMethod).toEqual('saml'); - return; - }, - ); await authOwnerAgent.post('/sso/saml/acs').expect(302); - expect(mockedHookOnUserLoginSuccess).toBeCalled(); - mockedHookOnUserLoginSuccess.mockRestore(); + mockedHandleSamlLogin.mockRestore(); }); @@ -332,23 +319,8 @@ describe('SAML login flow', () => { }, ); - const mockedHookOnUserLoginFailed = jest.spyOn( - Container.get(InternalHooks), - 'onUserLoginFailed', - ); - mockedHookOnUserLoginFailed.mockImplementation( - async (userLoginData: { - user: string; - authenticationMethod: AuthenticationMethod; - reason?: string; - }) => { - expect(userLoginData.authenticationMethod).toEqual('saml'); - return; - }, - ); await authOwnerAgent.post('/sso/saml/acs').expect(401); - expect(mockedHookOnUserLoginFailed).toBeCalled(); - mockedHookOnUserLoginFailed.mockRestore(); + mockedHandleSamlLogin.mockRestore(); }); }); From e7a6826cea5ee19dc376aaaad1c3a215eacb0981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 14:23:00 +0200 Subject: [PATCH 14/25] Fix recovery tests --- .../__tests__/execution-recovery.service.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts index e40f4e78acb2e..81dd19c7ca251 100644 --- a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts +++ b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts @@ -20,6 +20,8 @@ import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessag import type { EventMessageTypes as EventMessage } from '@/eventbus/EventMessageClasses'; import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { NodeConnectionType } from 'n8n-workflow'; +import type { EventSender } from '@/eventbus/event-sender'; +import { mock } from 'jest-mock-extended'; /** * Workflow producing an execution whose data will be truncated by an instance crash. @@ -182,7 +184,11 @@ describe('ExecutionRecoveryService', () => { mockInstance(InternalHooks); push = mockInstance(Push); executionRepository = Container.get(ExecutionRepository); - executionRecoveryService = new ExecutionRecoveryService(push, executionRepository); + executionRecoveryService = new ExecutionRecoveryService( + push, + executionRepository, + mock(), + ); }); afterEach(async () => { From 808dd95fb1bbc6c04cef9a345471442f0b5fd4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 14:23:39 +0200 Subject: [PATCH 15/25] Fix workflow service tests --- packages/cli/test/integration/workflows/workflow.service.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/test/integration/workflows/workflow.service.test.ts b/packages/cli/test/integration/workflows/workflow.service.test.ts index 8c9e35983e47b..b09cf30b0d1dd 100644 --- a/packages/cli/test/integration/workflows/workflow.service.test.ts +++ b/packages/cli/test/integration/workflows/workflow.service.test.ts @@ -38,6 +38,7 @@ beforeAll(async () => { mock(), mock(), mock(), + mock(), ); }); From df6519c03e114a026055fa765b3321ef892a0c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 14:26:03 +0200 Subject: [PATCH 16/25] Fix internalhooks test --- packages/cli/test/unit/InternalHooks.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/test/unit/InternalHooks.test.ts b/packages/cli/test/unit/InternalHooks.test.ts index 6dbb4ab5f2def..f56b45d1b8b34 100644 --- a/packages/cli/test/unit/InternalHooks.test.ts +++ b/packages/cli/test/unit/InternalHooks.test.ts @@ -23,7 +23,6 @@ describe('InternalHooks', () => { mock(), mock(), mock(), - mock(), license, mock(), mock(), From abac50c6512b841aa323d21c808e491c1f6964eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 15:51:47 +0200 Subject: [PATCH 17/25] Cleanup --- packages/cli/src/InternalHooks.ts | 48 ++++++-------- .../handlers/workflows/workflows.handler.ts | 3 +- .../email/UserManagementMailer.ts | 9 +++ .../communityPackages.controller.ts | 4 +- packages/cli/src/controllers/me.controller.ts | 24 ++----- .../controllers/passwordReset.controller.ts | 1 + .../src/credentials/credentials.controller.ts | 1 - packages/cli/src/eventbus/audit.types.ts | 5 +- packages/cli/src/eventbus/auditor.service.ts | 22 +++---- packages/cli/src/services/user.service.ts | 1 + .../cli/src/workflows/workflow.service.ts | 8 ++- .../cli/src/workflows/workflows.controller.ts | 2 +- .../test/integration/saml/saml.api.test.ts | 64 ------------------- 13 files changed, 62 insertions(+), 130 deletions(-) diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index c4d091213ec11..45f9dd0f9d200 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -517,29 +517,23 @@ export class InternalHooks { } async onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise { - void Promise.all([ - this.telemetry.track('API key deleted', { - user_id: apiKeyDeletedData.user.id, - public_api: apiKeyDeletedData.public_api, - }), - ]); + void this.telemetry.track('API key deleted', { + user_id: apiKeyDeletedData.user.id, + public_api: apiKeyDeletedData.public_api, + }); } async onApiKeyCreated(apiKeyCreatedData: { user: User; public_api: boolean }): Promise { - void Promise.all([ - this.telemetry.track('API key created', { - user_id: apiKeyCreatedData.user.id, - public_api: apiKeyCreatedData.public_api, - }), - ]); + void this.telemetry.track('API key created', { + user_id: apiKeyCreatedData.user.id, + public_api: apiKeyCreatedData.public_api, + }); } async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise { - void Promise.all([ - this.telemetry.track('User requested password reset while logged out', { - user_id: userPasswordResetData.user.id, - }), - ]); + void this.telemetry.track('User requested password reset while logged out', { + user_id: userPasswordResetData.user.id, + }); } async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise { @@ -696,17 +690,15 @@ export class InternalHooks { package_author?: string; package_author_email?: string; }): Promise { - void Promise.all([ - this.telemetry.track('cnr package updated', { - user_id: updateData.user.id, - package_name: updateData.package_name, - package_version_current: updateData.package_version_current, - package_version_new: updateData.package_version_new, - package_node_names: updateData.package_node_names, - package_author: updateData.package_author, - package_author_email: updateData.package_author_email, - }), - ]); + void this.telemetry.track('cnr package updated', { + user_id: updateData.user.id, + package_name: updateData.package_name, + package_version_current: updateData.package_version_current, + package_version_new: updateData.package_version_new, + package_node_names: updateData.package_node_names, + package_author: updateData.package_author, + package_author_email: updateData.package_author_email, + }); } async onCommunityPackageDeleteFinished(deleteData: { diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index bdee0fece73a1..680274181b072 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -240,7 +240,8 @@ export = { void Container.get(InternalHooks).onWorkflowSaved(req.user, updateData, true); Container.get(EventSender).emit('workflow-saved', { user: req.user, - workflow: updateData, + workflowId: updateData.id, + workflowName: updateData.name, }); return res.json(updatedWorkflow); diff --git a/packages/cli/src/UserManagement/email/UserManagementMailer.ts b/packages/cli/src/UserManagement/email/UserManagementMailer.ts index d864ab7b2ee88..7377e30d5c37f 100644 --- a/packages/cli/src/UserManagement/email/UserManagementMailer.ts +++ b/packages/cli/src/UserManagement/email/UserManagementMailer.ts @@ -17,6 +17,7 @@ import { toError } from '@/utils'; import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces'; import { NodeMailer } from './NodeMailer'; +import { EventSender } from '@/eventbus/event-sender'; type Template = HandlebarsTemplateDelegate; type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared'; @@ -144,6 +145,10 @@ export class UserManagementMailer { message_type: 'Workflow shared', public_api: false, }); + Container.get(EventSender).emit('email-failed', { + user: sharer, + messageType: 'Workflow shared', + }); const error = toError(e); @@ -199,6 +204,10 @@ export class UserManagementMailer { message_type: 'Credentials shared', public_api: false, }); + Container.get(EventSender).emit('email-failed', { + user: sharer, + messageType: 'Credentials shared', + }); const error = toError(e); diff --git a/packages/cli/src/controllers/communityPackages.controller.ts b/packages/cli/src/controllers/communityPackages.controller.ts index dd1821c03fd51..965ab7b924ee6 100644 --- a/packages/cli/src/controllers/communityPackages.controller.ts +++ b/packages/cli/src/controllers/communityPackages.controller.ts @@ -118,11 +118,11 @@ export class CommunityPackagesController { }); this.eventSender.emit('community-package-installed', { user: req.user, + inputString: name, packageName: parsed.packageName, success: false, packageVersion: parsed.version, failureReason: errorMessage, - inputString: name, }); let message = [`Error loading package "${name}" `, errorMessage].join(':'); @@ -314,7 +314,7 @@ export class CommunityPackagesController { packageName: name, packageVersionCurrent: previouslyInstalledPackage.installedVersion, packageVersionNew: newInstalledPackage.installedVersion, - packageNodeNames: newInstalledPackage.installedNodes.map((node) => node.name), + packageNodeNames: newInstalledPackage.installedNodes.map((n) => n.name), packageAuthor: newInstalledPackage.authorName, packageAuthorEmail: newInstalledPackage.authorEmail, }); diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index a53fa35d63ecd..509a06a0adc79 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -98,12 +98,9 @@ export class MeController { this.authService.issueCookie(res, user, req.browserId); - const updatedKeys = Object.keys(payload); - void this.internalHooks.onUserUpdate({ - user, - fields_changed: updatedKeys, - }); - this.eventSender.emit('user-updated', { user, fieldsChanged: updatedKeys }); + const fieldsChanged = Object.keys(payload); + void this.internalHooks.onUserUpdate({ user, fields_changed: fieldsChanged }); + this.eventSender.emit('user-updated', { user, fieldsChanged }); const publicUser = await this.userService.toPublic(user); @@ -152,10 +149,7 @@ export class MeController { this.authService.issueCookie(res, updatedUser, req.browserId); - void this.internalHooks.onUserUpdate({ - user: updatedUser, - fields_changed: ['password'], - }); + void this.internalHooks.onUserUpdate({ user: updatedUser, fields_changed: ['password'] }); this.eventSender.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] }); await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]); @@ -204,10 +198,7 @@ export class MeController { await this.userService.update(req.user.id, { apiKey }); - void this.internalHooks.onApiKeyCreated({ - user: req.user, - public_api: false, - }); + void this.internalHooks.onApiKeyCreated({ user: req.user, public_api: false }); this.eventSender.emit('api-key-created', { user: req.user }); return { apiKey }; @@ -228,10 +219,7 @@ export class MeController { async deleteAPIKey(req: AuthenticatedRequest) { await this.userService.update(req.user.id, { apiKey: null }); - void this.internalHooks.onApiKeyDeleted({ - user: req.user, - public_api: false, - }); + void this.internalHooks.onApiKeyDeleted({ user: req.user, public_api: false }); this.eventSender.emit('api-key-deleted', { user: req.user }); return { success: true }; diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 65af3b3096069..20e5c1f393686 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -125,6 +125,7 @@ export class PasswordResetController { message_type: 'Reset password', public_api: false, }); + this.eventSender.emit('email-failed', { user, messageType: 'Reset password' }); if (error instanceof Error) { throw new InternalServerError(`Please contact your administrator: ${error.message}`); } diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index d5f65022c317f..1fcdb05510799 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -158,7 +158,6 @@ export class CredentialsController { credential_id: credential.id, public_api: false, }); - this.eventSender.emit('credentials-created', { user: req.user, credentialName: newCredential.name, diff --git a/packages/cli/src/eventbus/audit.types.ts b/packages/cli/src/eventbus/audit.types.ts index 7575fd12ca2cc..e58c81765e436 100644 --- a/packages/cli/src/eventbus/audit.types.ts +++ b/packages/cli/src/eventbus/audit.types.ts @@ -17,12 +17,13 @@ export type AuditEventArgs = { 'workflow-deleted': { user: UserLike; - workflow: IWorkflowBase; + workflowId: string; }; 'workflow-saved': { user: UserLike; - workflow: IWorkflowBase; + workflowId: string; + workflowName: string; }; 'workflow-pre-execute': { diff --git a/packages/cli/src/eventbus/auditor.service.ts b/packages/cli/src/eventbus/auditor.service.ts index 38762fafe8a8c..fd2734fd1de4e 100644 --- a/packages/cli/src/eventbus/auditor.service.ts +++ b/packages/cli/src/eventbus/auditor.service.ts @@ -68,21 +68,21 @@ export class AuditorService extends EventEmitter { } @Redactable() - private workflowDeleted({ user, workflow }: AuditEventArgs['workflow-deleted']) { + private workflowDeleted({ user, workflowId }: AuditEventArgs['workflow-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.deleted', - payload: { ...user, workflow: workflow.id }, + payload: { ...user, workflowId }, }); } @Redactable() - private workflowSaved({ user, workflow }: AuditEventArgs['workflow-saved']) { + private workflowSaved({ user, workflowId, workflowName }: AuditEventArgs['workflow-saved']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.updated', payload: { ...user, - workflowId: workflow.id, - workflowName: workflow.name, + workflowId, + workflowName, }, }); } @@ -122,32 +122,32 @@ export class AuditorService extends EventEmitter { * Node */ - private nodePreExecute({ executionId, workflow, nodeName }: AuditEventArgs['node-pre-execute']) { + private nodePreExecute({ workflow, executionId, nodeName }: AuditEventArgs['node-pre-execute']) { void this.eventBus.sendNodeEvent({ eventName: 'n8n.node.started', payload: { - executionId, - nodeName, workflowId: workflow.id, workflowName: workflow.name, + executionId, nodeType: workflow.nodes.find((n) => n.name === nodeName)?.type, + nodeName, }, }); } private nodePostExecute({ + workflow, executionId, nodeName, - workflow, }: AuditEventArgs['node-post-execute']) { void this.eventBus.sendNodeEvent({ eventName: 'n8n.node.finished', payload: { - executionId, - nodeName, workflowId: workflow.id, workflowName: workflow.name, + executionId, nodeType: workflow.nodes.find((n) => n.name === nodeName)?.type, + nodeName, }, }); } diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index 3c403cf33cfb2..7749cd3477544 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -169,6 +169,7 @@ export class UserService { message_type: 'New user invite', public_api: false, }); + this.eventSender.emit('email-failed', { user: owner, messageType: 'New user invite' }); this.logger.error('Failed to send email', { userId: owner.id, inviteAcceptUrl, diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 4742e4bd2f186..44b5dc273c3b9 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -218,7 +218,11 @@ export class WorkflowService { await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]); void Container.get(InternalHooks).onWorkflowSaved(user, updatedWorkflow, false); - this.eventSender.emit('workflow-saved', { user, workflow: updatedWorkflow }); + this.eventSender.emit('workflow-saved', { + user, + workflowId: updatedWorkflow.id, + workflowName: updatedWorkflow.name, + }); if (updatedWorkflow.active) { // When the workflow is supposed to be active add it again @@ -277,7 +281,7 @@ export class WorkflowService { await this.binaryDataService.deleteMany(idsForDeletion); void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false); - this.eventSender.emit('workflow-deleted', { user, workflow }); + this.eventSender.emit('workflow-deleted', { user, workflowId }); await this.externalHooks.run('workflow.afterDelete', [workflowId]); return workflow; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 282a95cde3472..40d8c03bf6e57 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -177,7 +177,7 @@ export class WorkflowsController { await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]); void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false); - this.eventSender.emit('workflow-created', { user: req.user, workflow: savedWorkflow }); + this.eventSender.emit('workflow-created', { user: req.user, workflow: newWorkflow }); const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id); diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index 2b26f94119efd..23dc49607f709 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -1,10 +1,6 @@ -import { Container } from 'typedi'; - import type { User } from '@db/entities/User'; import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; -import { SamlService } from '@/sso/saml/saml.service.ee'; -import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes'; import { randomEmail, randomName, randomValidPassword } from '../shared/random'; import * as utils from '../shared/utils/'; @@ -264,63 +260,3 @@ describe('Check endpoint permissions', () => { }); }); }); - -describe('SAML login flow', () => { - beforeEach(async () => { - await enableSaml(true); - }); - - test('should trigger onUserLoginSuccess hook', async () => { - const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin'); - - mockedHandleSamlLogin.mockImplementation( - async (): Promise<{ - authenticatedUser: User; - attributes: SamlUserAttributes; - onboardingRequired: false; - }> => { - return { - authenticatedUser: someUser, - attributes: { - email: someUser.email, - firstName: someUser.firstName, - lastName: someUser.lastName, - userPrincipalName: someUser.email, - }, - onboardingRequired: false, - }; - }, - ); - - await authOwnerAgent.post('/sso/saml/acs').expect(302); - - mockedHandleSamlLogin.mockRestore(); - }); - - test('should trigger onUserLoginFailed hook', async () => { - const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin'); - - mockedHandleSamlLogin.mockImplementation( - async (): Promise<{ - authenticatedUser: User | undefined; - attributes: SamlUserAttributes; - onboardingRequired: false; - }> => { - return { - authenticatedUser: undefined, - attributes: { - email: someUser.email, - firstName: someUser.firstName, - lastName: someUser.lastName, - userPrincipalName: someUser.email, - }, - onboardingRequired: false, - }; - }, - ); - - await authOwnerAgent.post('/sso/saml/acs').expect(401); - - mockedHandleSamlLogin.mockRestore(); - }); -}); From b2e0cf41f659ce9f76d41c6f3c12f12f7f84eea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 14 Jun 2024 15:53:41 +0200 Subject: [PATCH 18/25] Remove unneeded `Promise.all` --- packages/cli/src/InternalHooks.ts | 150 +++++++++++++----------------- 1 file changed, 64 insertions(+), 86 deletions(-) diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 45f9dd0f9d200..2b6d7178aac4e 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -464,31 +464,25 @@ export class InternalHooks { } async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise { - void Promise.all([ - this.telemetry.track('User changed personal settings', { - user_id: userUpdateData.user.id, - fields_changed: userUpdateData.fields_changed, - }), - ]); + void this.telemetry.track('User changed personal settings', { + user_id: userUpdateData.user.id, + fields_changed: userUpdateData.fields_changed, + }); } async onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User; }): Promise { - void Promise.all([ - this.telemetry.track('User clicked invite link from email', { - user_id: userInviteClickData.invitee.id, - }), - ]); + void this.telemetry.track('User clicked invite link from email', { + user_id: userInviteClickData.invitee.id, + }); } async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise { - void Promise.all([ - this.telemetry.track('User clicked password reset link from email', { - user_id: userPasswordResetData.user.id, - }), - ]); + void this.telemetry.track('User clicked password reset link from email', { + user_id: userPasswordResetData.user.id, + }); } async onUserTransactionalEmail(userTransactionalEmailData: { @@ -547,12 +541,10 @@ export class InternalHooks { was_disabled_ldap_user: boolean; }, ): Promise { - void Promise.all([ - this.telemetry.track('User signed up', { - user_id: user.id, - ...userSignupData, - }), - ]); + void this.telemetry.track('User signed up', { + user_id: user.id, + ...userSignupData, + }); } async onEmailFailed(failedEmailData: { @@ -565,11 +557,9 @@ export class InternalHooks { | 'Credentials shared'; public_api: boolean; }): Promise { - void Promise.all([ - this.telemetry.track('Instance failed to send transactional email to user', { - user_id: failedEmailData.user.id, - }), - ]); + void this.telemetry.track('Instance failed to send transactional email to user', { + user_id: failedEmailData.user.id, + }); } /** @@ -586,16 +576,14 @@ export class InternalHooks { const project = await this.sharedCredentialsRepository.findCredentialOwningProject( userCreatedCredentialsData.credential_id, ); - void Promise.all([ - this.telemetry.track('User created credentials', { - user_id: userCreatedCredentialsData.user.id, - credential_type: userCreatedCredentialsData.credential_type, - credential_id: userCreatedCredentialsData.credential_id, - instance_id: this.instanceSettings.instanceId, - project_id: project?.id, - project_type: project?.type, - }), - ]); + void this.telemetry.track('User created credentials', { + user_id: userCreatedCredentialsData.user.id, + credential_type: userCreatedCredentialsData.credential_type, + credential_id: userCreatedCredentialsData.credential_id, + instance_id: this.instanceSettings.instanceId, + project_id: project?.id, + project_type: project?.type, + }); } async onUserSharedCredentials(userSharedCredentialsData: { @@ -607,17 +595,15 @@ export class InternalHooks { user_ids_sharees_added: string[]; sharees_removed: number | null; }): Promise { - void Promise.all([ - this.telemetry.track('User updated cred sharing', { - user_id: userSharedCredentialsData.user.id, - credential_type: userSharedCredentialsData.credential_type, - credential_id: userSharedCredentialsData.credential_id, - user_id_sharer: userSharedCredentialsData.user_id_sharer, - user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added, - sharees_removed: userSharedCredentialsData.sharees_removed, - instance_id: this.instanceSettings.instanceId, - }), - ]); + void this.telemetry.track('User updated cred sharing', { + user_id: userSharedCredentialsData.user.id, + credential_type: userSharedCredentialsData.credential_type, + credential_id: userSharedCredentialsData.credential_id, + user_id_sharer: userSharedCredentialsData.user_id_sharer, + user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added, + sharees_removed: userSharedCredentialsData.sharees_removed, + instance_id: this.instanceSettings.instanceId, + }); } async onUserUpdatedCredentials(userUpdatedCredentialsData: { @@ -626,13 +612,11 @@ export class InternalHooks { credential_type: string; credential_id: string; }): Promise { - void Promise.all([ - this.telemetry.track('User updated credentials', { - user_id: userUpdatedCredentialsData.user.id, - credential_type: userUpdatedCredentialsData.credential_type, - credential_id: userUpdatedCredentialsData.credential_id, - }), - ]); + void this.telemetry.track('User updated credentials', { + user_id: userUpdatedCredentialsData.user.id, + credential_type: userUpdatedCredentialsData.credential_type, + credential_id: userUpdatedCredentialsData.credential_id, + }); } async onUserDeletedCredentials(userUpdatedCredentialsData: { @@ -641,14 +625,12 @@ export class InternalHooks { credential_type: string; credential_id: string; }): Promise { - void Promise.all([ - this.telemetry.track('User deleted credentials', { - user_id: userUpdatedCredentialsData.user.id, - credential_type: userUpdatedCredentialsData.credential_type, - credential_id: userUpdatedCredentialsData.credential_id, - instance_id: this.instanceSettings.instanceId, - }), - ]); + void this.telemetry.track('User deleted credentials', { + user_id: userUpdatedCredentialsData.user.id, + credential_type: userUpdatedCredentialsData.credential_type, + credential_id: userUpdatedCredentialsData.credential_id, + instance_id: this.instanceSettings.instanceId, + }); } /** @@ -666,19 +648,17 @@ export class InternalHooks { package_author_email?: string; failure_reason?: string; }): Promise { - void Promise.all([ - this.telemetry.track('cnr package install finished', { - user_id: installationData.user.id, - input_string: installationData.input_string, - package_name: installationData.package_name, - success: installationData.success, - package_version: installationData.package_version, - package_node_names: installationData.package_node_names, - package_author: installationData.package_author, - package_author_email: installationData.package_author_email, - failure_reason: installationData.failure_reason, - }), - ]); + void this.telemetry.track('cnr package install finished', { + user_id: installationData.user.id, + input_string: installationData.input_string, + package_name: installationData.package_name, + success: installationData.success, + package_version: installationData.package_version, + package_node_names: installationData.package_node_names, + package_author: installationData.package_author, + package_author_email: installationData.package_author_email, + failure_reason: installationData.failure_reason, + }); } async onCommunityPackageUpdateFinished(updateData: { @@ -709,16 +689,14 @@ export class InternalHooks { package_author?: string; package_author_email?: string; }): Promise { - void Promise.all([ - this.telemetry.track('cnr package deleted', { - user_id: deleteData.user.id, - package_name: deleteData.package_name, - package_version: deleteData.package_version, - package_node_names: deleteData.package_node_names, - package_author: deleteData.package_author, - package_author_email: deleteData.package_author_email, - }), - ]); + void this.telemetry.track('cnr package deleted', { + user_id: deleteData.user.id, + package_name: deleteData.package_name, + package_version: deleteData.package_version, + package_node_names: deleteData.package_node_names, + package_author: deleteData.package_author, + package_author_email: deleteData.package_author_email, + }); } async onLdapSyncFinished(data: { From d60cd175a546d5a4a139bd32c9ab10ad1a95c93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 17 Jun 2024 10:04:35 +0200 Subject: [PATCH 19/25] Apply suggestion --- .../credentials/credentials.service.ts | 6 +- .../handlers/workflows/workflows.handler.ts | 6 +- .../email/UserManagementMailer.ts | 6 +- .../cli/src/WorkflowExecuteAdditionalData.ts | 24 ++--- packages/cli/src/WorkflowRunner.ts | 8 +- packages/cli/src/auth/methods/ldap.ts | 4 +- .../cli/src/controllers/auth.controller.ts | 10 +- .../communityPackages.controller.ts | 12 +-- .../src/controllers/invitation.controller.ts | 6 +- packages/cli/src/controllers/me.controller.ts | 12 +-- .../controllers/passwordReset.controller.ts | 14 +-- .../cli/src/controllers/users.controller.ts | 6 +- .../src/credentials/credentials.controller.ts | 12 +-- ...t.ts => audit-event-relay.service.test.ts} | 6 +- ...ervice.ts => audit-event-relay.service.ts} | 92 +++++++++---------- ...event-sender.ts => event-relay.service.ts} | 14 +-- .../execution-recovery.service.test.ts | 4 +- .../executions/execution-recovery.service.ts | 6 +- packages/cli/src/services/user.service.ts | 8 +- .../src/sso/saml/routes/saml.controller.ee.ts | 12 +-- .../cli/src/workflows/workflow.service.ts | 8 +- .../cli/src/workflows/workflows.controller.ts | 6 +- 22 files changed, 142 insertions(+), 140 deletions(-) rename packages/cli/src/eventbus/__tests__/{auditor.service.test.ts => audit-event-relay.service.test.ts} (88%) rename packages/cli/src/eventbus/{auditor.service.ts => audit-event-relay.service.ts} (70%) rename packages/cli/src/eventbus/{event-sender.ts => event-relay.service.ts} (51%) diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts index 58be809859e6b..adb7d370e6861 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts @@ -18,7 +18,7 @@ import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { InternalHooks } from '@/InternalHooks'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; export async function getCredentials(credentialId: string): Promise { return await Container.get(CredentialsRepository).findOneBy({ id: credentialId }); @@ -60,7 +60,7 @@ export async function saveCredential( credential_id: credential.id, public_api: true, }); - Container.get(EventSender).emit('credentials-created', { + Container.get(EventRelay).emit('credentials-created', { user, credentialName: credential.name, credentialType: credential.type, @@ -102,7 +102,7 @@ export async function removeCredential( credential_type: credentials.type, credential_id: credentials.id, }); - Container.get(EventSender).emit('credentials-deleted', { + Container.get(EventRelay).emit('credentials-deleted', { user, credentialName: credentials.name, credentialType: credentials.type, diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index 680274181b072..d0f69cd80d583 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -30,7 +30,7 @@ import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflo import { TagRepository } from '@/databases/repositories/tag.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; export = { createWorkflow: [ @@ -57,7 +57,7 @@ export = { await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]); void Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, project, true); - Container.get(EventSender).emit('workflow-created', { + Container.get(EventRelay).emit('workflow-created', { workflow: createdWorkflow, user: req.user, }); @@ -238,7 +238,7 @@ export = { await Container.get(ExternalHooks).run('workflow.afterUpdate', [updateData]); void Container.get(InternalHooks).onWorkflowSaved(req.user, updateData, true); - Container.get(EventSender).emit('workflow-saved', { + Container.get(EventRelay).emit('workflow-saved', { user: req.user, workflowId: updateData.id, workflowName: updateData.name, diff --git a/packages/cli/src/UserManagement/email/UserManagementMailer.ts b/packages/cli/src/UserManagement/email/UserManagementMailer.ts index 7377e30d5c37f..c6adec6e4d8d5 100644 --- a/packages/cli/src/UserManagement/email/UserManagementMailer.ts +++ b/packages/cli/src/UserManagement/email/UserManagementMailer.ts @@ -17,7 +17,7 @@ import { toError } from '@/utils'; import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces'; import { NodeMailer } from './NodeMailer'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; type Template = HandlebarsTemplateDelegate; type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared'; @@ -145,7 +145,7 @@ export class UserManagementMailer { message_type: 'Workflow shared', public_api: false, }); - Container.get(EventSender).emit('email-failed', { + Container.get(EventRelay).emit('email-failed', { user: sharer, messageType: 'Workflow shared', }); @@ -204,7 +204,7 @@ export class UserManagementMailer { message_type: 'Credentials shared', public_api: false, }); - Container.get(EventSender).emit('email-failed', { + Container.get(EventRelay).emit('email-failed', { user: sharer, messageType: 'Credentials shared', }); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index fe4ca20a04d3c..5b1804daac165 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -71,7 +71,7 @@ import { WorkflowRepository } from './databases/repositories/workflow.repository import { UrlService } from './services/url.service'; import { WorkflowExecutionService } from './workflows/workflowExecution.service'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; -import { EventSender } from './eventbus/event-sender'; +import { EventRelay } from './eventbus/event-relay.service'; const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); @@ -394,20 +394,20 @@ export function hookFunctionsPreExecute(): IWorkflowExecuteHooks { function hookFunctionsSave(): IWorkflowExecuteHooks { const logger = Container.get(Logger); const eventsService = Container.get(EventsService); - const eventSender = Container.get(EventSender); + const eventRelay = Container.get(EventRelay); return { nodeExecuteBefore: [ async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - eventSender.emit('node-pre-execute', { executionId, workflow, nodeName }); + eventRelay.emit('node-pre-execute', { executionId, workflow, nodeName }); }, ], nodeExecuteAfter: [ async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - eventSender.emit('node-post-execute', { executionId, workflow, nodeName }); + eventRelay.emit('node-post-execute', { executionId, workflow, nodeName }); }, ], workflowExecuteBefore: [], @@ -546,27 +546,27 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { const logger = Container.get(Logger); const internalHooks = Container.get(InternalHooks); const eventsService = Container.get(EventsService); - const eventSender = Container.get(EventSender); + const eventRelay = Container.get(EventRelay); return { nodeExecuteBefore: [ async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - eventSender.emit('node-pre-execute', { executionId, workflow, nodeName }); + eventRelay.emit('node-pre-execute', { executionId, workflow, nodeName }); }, ], nodeExecuteAfter: [ async function (this: WorkflowHooks, nodeName: string): Promise { const { executionId, workflowData: workflow } = this; - eventSender.emit('node-post-execute', { executionId, workflow, nodeName }); + eventRelay.emit('node-post-execute', { executionId, workflow, nodeName }); }, ], workflowExecuteBefore: [ async function (): Promise { const { executionId, workflowData } = this; - eventSender.emit('workflow-pre-execute', { executionId, data: workflowData }); + eventRelay.emit('workflow-pre-execute', { executionId, data: workflowData }); }, ], workflowExecuteAfter: [ @@ -638,7 +638,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { const { executionId, workflowData: workflow } = this; void internalHooks.onWorkflowPostExecute(executionId, workflow, runData); - eventSender.emit('workflow-post-execute', { + eventRelay.emit('workflow-post-execute', { workflowId: workflow.id, workflowName: workflow.name, executionId, @@ -785,7 +785,7 @@ async function executeWorkflow( const nodeTypes = Container.get(NodeTypes); const activeExecutions = Container.get(ActiveExecutions); - const eventSender = Container.get(EventSender); + const eventRelay = Container.get(EventRelay); const workflowData = options.loadedWorkflowData ?? @@ -813,7 +813,7 @@ async function executeWorkflow( executionId = options.parentExecutionId ?? (await activeExecutions.add(runData)); } - Container.get(EventSender).emit('workflow-pre-execute', { executionId, data: runData }); + Container.get(EventRelay).emit('workflow-pre-execute', { executionId, data: runData }); let data; try { @@ -926,7 +926,7 @@ async function executeWorkflow( await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]); void internalHooks.onWorkflowPostExecute(executionId, workflowData, data, additionalData.userId); - eventSender.emit('workflow-post-execute', { + eventRelay.emit('workflow-post-execute', { workflowId: workflowData.id, workflowName: workflowData.name, executionId, diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 30b0d5420e5be..8430d95327d60 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -37,7 +37,7 @@ import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { InternalHooks } from '@/InternalHooks'; import { Logger } from '@/Logger'; import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; -import { EventSender } from './eventbus/event-sender'; +import { EventRelay } from './eventbus/event-relay.service'; @Service() export class WorkflowRunner { @@ -53,7 +53,7 @@ export class WorkflowRunner { private readonly workflowStaticDataService: WorkflowStaticDataService, private readonly nodeTypes: NodeTypes, private readonly permissionChecker: PermissionChecker, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) { if (this.executionsMode === 'queue') { this.jobQueue = Container.get(Queue); @@ -147,7 +147,7 @@ export class WorkflowRunner { await this.enqueueExecution(executionId, data, loadStaticData, realtime); } else { await this.runMainProcess(executionId, data, loadStaticData, executionId); - this.eventSender.emit('workflow-pre-execute', { executionId, data }); + this.eventRelay.emit('workflow-pre-execute', { executionId, data }); } // only run these when not in queue mode or when the execution is manual, @@ -166,7 +166,7 @@ export class WorkflowRunner { executionData, data.userId, ); - this.eventSender.emit('workflow-post-execute', { + this.eventRelay.emit('workflow-post-execute', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId, diff --git a/packages/cli/src/auth/methods/ldap.ts b/packages/cli/src/auth/methods/ldap.ts index be6fc512e5c90..d7a254889e8f3 100644 --- a/packages/cli/src/auth/methods/ldap.ts +++ b/packages/cli/src/auth/methods/ldap.ts @@ -12,7 +12,7 @@ import { updateLdapUserOnLocalDb, } from '@/Ldap/helpers'; import type { User } from '@db/entities/User'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; export const handleLdapLogin = async ( loginId: string, @@ -55,7 +55,7 @@ export const handleLdapLogin = async ( user_type: 'ldap', was_disabled_ldap_user: false, }); - Container.get(EventSender).emit('user-signed-up', { user }); + Container.get(EventRelay).emit('user-signed-up', { user }); return user; } } else { diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 4cbcaa5fe264b..a0dbac374da58 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -24,7 +24,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ApplicationError } from 'n8n-workflow'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController() export class AuthController { @@ -36,7 +36,7 @@ export class AuthController { private readonly userService: UserService, private readonly license: License, private readonly userRepository: UserRepository, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, private readonly postHog?: PostHogClient, ) {} @@ -93,14 +93,14 @@ export class AuthController { this.authService.issueCookie(res, user, req.browserId); - this.eventSender.emit('user-logged-in', { + this.eventRelay.emit('user-logged-in', { user, authenticationMethod: usedAuthenticationMethod, }); return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true }); } - this.eventSender.emit('user-login-failed', { + this.eventRelay.emit('user-login-failed', { authenticationMethod: usedAuthenticationMethod, userEmail: email, }); @@ -179,7 +179,7 @@ export class AuthController { } void this.internalHooks.onUserInviteEmailClick({ inviter, invitee }); - this.eventSender.emit('user-invite-email-click', { inviter, invitee }); + this.eventRelay.emit('user-invite-email-click', { inviter, invitee }); const { firstName, lastName } = inviter; return { inviter: { firstName, lastName } }; diff --git a/packages/cli/src/controllers/communityPackages.controller.ts b/packages/cli/src/controllers/communityPackages.controller.ts index 965ab7b924ee6..11914e5d7a5cd 100644 --- a/packages/cli/src/controllers/communityPackages.controller.ts +++ b/packages/cli/src/controllers/communityPackages.controller.ts @@ -14,7 +14,7 @@ import { Push } from '@/push'; import { CommunityPackagesService } from '@/services/communityPackages.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; const { PACKAGE_NOT_INSTALLED, @@ -39,7 +39,7 @@ export class CommunityPackagesController { private readonly push: Push, private readonly internalHooks: InternalHooks, private readonly communityPackagesService: CommunityPackagesService, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` @@ -116,7 +116,7 @@ export class CommunityPackagesController { package_version: parsed.version, failure_reason: errorMessage, }); - this.eventSender.emit('community-package-installed', { + this.eventRelay.emit('community-package-installed', { user: req.user, inputString: name, packageName: parsed.packageName, @@ -154,7 +154,7 @@ export class CommunityPackagesController { package_author: installedPackage.authorName, package_author_email: installedPackage.authorEmail, }); - this.eventSender.emit('community-package-installed', { + this.eventRelay.emit('community-package-installed', { user: req.user, inputString: name, packageName: parsed.packageName, @@ -253,7 +253,7 @@ export class CommunityPackagesController { package_author: installedPackage.authorName, package_author_email: installedPackage.authorEmail, }); - this.eventSender.emit('community-package-deleted', { + this.eventRelay.emit('community-package-deleted', { user: req.user, packageName: name, packageVersion: installedPackage.installedVersion, @@ -309,7 +309,7 @@ export class CommunityPackagesController { package_author: newInstalledPackage.authorName, package_author_email: newInstalledPackage.authorEmail, }); - this.eventSender.emit('community-package-updated', { + this.eventRelay.emit('community-package-updated', { user: req.user, packageName: name, packageVersionCurrent: previouslyInstalledPackage.installedVersion, diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index 6db39c93c1ba2..9f1013b29cd0f 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -18,7 +18,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { InternalHooks } from '@/InternalHooks'; import { ExternalHooks } from '@/ExternalHooks'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController('/invitations') export class InvitationController { @@ -32,7 +32,7 @@ export class InvitationController { private readonly passwordUtility: PasswordUtility, private readonly userRepository: UserRepository, private readonly postHog: PostHogClient, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} /** @@ -172,7 +172,7 @@ export class InvitationController { user_type: 'email', was_disabled_ldap_user: false, }); - this.eventSender.emit('user-signed-up', { user: updatedUser }); + this.eventRelay.emit('user-signed-up', { user: updatedUser }); const publicInvitee = await this.userService.toPublic(invitee); diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 509a06a0adc79..57eafe4a75f49 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -23,7 +23,7 @@ import { InternalHooks } from '@/InternalHooks'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { UserRepository } from '@/databases/repositories/user.repository'; import { isApiEnabled } from '@/PublicApi'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => { if (isApiEnabled()) { @@ -43,7 +43,7 @@ export class MeController { private readonly userService: UserService, private readonly passwordUtility: PasswordUtility, private readonly userRepository: UserRepository, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} /** @@ -100,7 +100,7 @@ export class MeController { const fieldsChanged = Object.keys(payload); void this.internalHooks.onUserUpdate({ user, fields_changed: fieldsChanged }); - this.eventSender.emit('user-updated', { user, fieldsChanged }); + this.eventRelay.emit('user-updated', { user, fieldsChanged }); const publicUser = await this.userService.toPublic(user); @@ -150,7 +150,7 @@ export class MeController { this.authService.issueCookie(res, updatedUser, req.browserId); void this.internalHooks.onUserUpdate({ user: updatedUser, fields_changed: ['password'] }); - this.eventSender.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] }); + this.eventRelay.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] }); await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]); @@ -199,7 +199,7 @@ export class MeController { await this.userService.update(req.user.id, { apiKey }); void this.internalHooks.onApiKeyCreated({ user: req.user, public_api: false }); - this.eventSender.emit('api-key-created', { user: req.user }); + this.eventRelay.emit('api-key-created', { user: req.user }); return { apiKey }; } @@ -220,7 +220,7 @@ export class MeController { await this.userService.update(req.user.id, { apiKey: null }); void this.internalHooks.onApiKeyDeleted({ user: req.user, public_api: false }); - this.eventSender.emit('api-key-deleted', { user: req.user }); + this.eventRelay.emit('api-key-deleted', { user: req.user }); return { success: true }; } diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 20e5c1f393686..aa053d0f525b8 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -21,7 +21,7 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController() export class PasswordResetController { @@ -37,7 +37,7 @@ export class PasswordResetController { private readonly license: License, private readonly passwordUtility: PasswordUtility, private readonly userRepository: UserRepository, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} /** @@ -125,7 +125,7 @@ export class PasswordResetController { message_type: 'Reset password', public_api: false, }); - this.eventSender.emit('email-failed', { user, messageType: 'Reset password' }); + this.eventRelay.emit('email-failed', { user, messageType: 'Reset password' }); if (error instanceof Error) { throw new InternalServerError(`Please contact your administrator: ${error.message}`); } @@ -139,7 +139,7 @@ export class PasswordResetController { }); void this.internalHooks.onUserPasswordResetRequestClick({ user }); - this.eventSender.emit('user-password-reset-request-click', { user }); + this.eventRelay.emit('user-password-reset-request-click', { user }); } /** @@ -172,7 +172,7 @@ export class PasswordResetController { this.logger.info('Reset-password token resolved successfully', { userId: user.id }); void this.internalHooks.onUserPasswordResetEmailClick({ user }); - this.eventSender.emit('user-password-reset-email-click', { user }); + this.eventRelay.emit('user-password-reset-email-click', { user }); } /** @@ -216,7 +216,7 @@ export class PasswordResetController { this.authService.issueCookie(res, user, req.browserId); void this.internalHooks.onUserUpdate({ user, fields_changed: ['password'] }); - this.eventSender.emit('user-updated', { user, fieldsChanged: ['password'] }); + this.eventRelay.emit('user-updated', { user, fieldsChanged: ['password'] }); // if this user used to be an LDAP users const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap'); @@ -225,7 +225,7 @@ export class PasswordResetController { user_type: 'email', was_disabled_ldap_user: true, }); - this.eventSender.emit('user-signed-up', { user }); + this.eventRelay.emit('user-signed-up', { user }); } await this.externalHooks.run('user.password.update', [user.email, passwordHash]); diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 5be19a2bf7c87..76206330dbf82 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -28,7 +28,7 @@ import { Project } from '@/databases/entities/Project'; import { WorkflowService } from '@/workflows/workflow.service'; import { CredentialsService } from '@/credentials/credentials.service'; import { ProjectService } from '@/services/project.service'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController('/users') export class UsersController { @@ -45,7 +45,7 @@ export class UsersController { private readonly workflowService: WorkflowService, private readonly credentialsService: CredentialsService, private readonly projectService: ProjectService, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} static ERROR_MESSAGES = { @@ -258,7 +258,7 @@ export class UsersController { telemetryData, publicApi: false, }); - this.eventSender.emit('user-deleted', { user: req.user }); + this.eventRelay.emit('user-deleted', { user: req.user }); await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]); diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 1fcdb05510799..ec0f26fdb8120 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -29,7 +29,7 @@ import { In } from '@n8n/typeorm'; import { SharedCredentials } from '@/databases/entities/SharedCredentials'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import { z } from 'zod'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController('/credentials') export class CredentialsController { @@ -43,7 +43,7 @@ export class CredentialsController { private readonly userManagementMailer: UserManagementMailer, private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly projectRelationRepository: ProjectRelationRepository, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} @Get('/', { middlewares: listQueryMiddleware }) @@ -158,7 +158,7 @@ export class CredentialsController { credential_id: credential.id, public_api: false, }); - this.eventSender.emit('credentials-created', { + this.eventRelay.emit('credentials-created', { user: req.user, credentialName: newCredential.name, credentialType: credential.type, @@ -218,7 +218,7 @@ export class CredentialsController { credential_type: credential.type, credential_id: credential.id, }); - this.eventSender.emit('credentials-updated', { + this.eventRelay.emit('credentials-updated', { user: req.user, credentialName: credential.name, credentialType: credential.type, @@ -259,7 +259,7 @@ export class CredentialsController { credential_type: credential.type, credential_id: credential.id, }); - this.eventSender.emit('credentials-deleted', { + this.eventRelay.emit('credentials-deleted', { user: req.user, credentialName: credential.name, credentialType: credential.type, @@ -333,7 +333,7 @@ export class CredentialsController { user_ids_sharees_added: newShareeIds, sharees_removed: amountRemoved, }); - this.eventSender.emit('credentials-shared', { + this.eventRelay.emit('credentials-shared', { user: req.user, credentialName: credential.name, credentialType: credential.type, diff --git a/packages/cli/src/eventbus/__tests__/auditor.service.test.ts b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts similarity index 88% rename from packages/cli/src/eventbus/__tests__/auditor.service.test.ts rename to packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts index 3cecd258981dc..3bace4c0a4348 100644 --- a/packages/cli/src/eventbus/__tests__/auditor.service.test.ts +++ b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts @@ -1,11 +1,13 @@ import { mock } from 'jest-mock-extended'; +import { AuditEventRelay } from '../audit-event-relay.service'; import type { MessageEventBus } from '../MessageEventBus/MessageEventBus'; -import { AuditorService } from '../auditor.service'; import type { AuditEventArgs } from '../audit.types'; +import type { EventRelay } from '../event-relay.service'; describe('AuditorService', () => { const eventBus = mock(); - const auditor = new AuditorService(eventBus); + const eventRelay = mock(); + const auditor = new AuditEventRelay(eventRelay, eventBus); afterEach(() => { jest.clearAllMocks(); diff --git a/packages/cli/src/eventbus/auditor.service.ts b/packages/cli/src/eventbus/audit-event-relay.service.ts similarity index 70% rename from packages/cli/src/eventbus/auditor.service.ts rename to packages/cli/src/eventbus/audit-event-relay.service.ts index fd2734fd1de4e..4fac8e636ac68 100644 --- a/packages/cli/src/eventbus/auditor.service.ts +++ b/packages/cli/src/eventbus/audit-event-relay.service.ts @@ -1,54 +1,54 @@ -import { EventEmitter } from 'node:events'; import { Service } from 'typedi'; import { MessageEventBus } from './MessageEventBus/MessageEventBus'; import { Redactable } from '@/decorators/Redactable'; +import { EventRelay } from './event-relay.service'; import type { AuditEventArgs } from './audit.types'; import type { IWorkflowBase } from 'n8n-workflow'; -/** - * Service for adding events from `EventSender` to the audit log. - */ @Service() -export class AuditorService extends EventEmitter { - constructor(private readonly eventBus: MessageEventBus) { - super(); +export class AuditEventRelay { + constructor( + private readonly eventRelay: EventRelay, + private readonly eventBus: MessageEventBus, + ) { this.setupHandlers(); } - on(eventName: K, handler: (arg: AuditEventArgs[K]) => void) { - super.on(eventName, handler); - return this; - } - private setupHandlers() { - this.on('workflow-created', (arg) => this.workflowCreated(arg)); - this.on('workflow-deleted', (arg) => this.workflowDeleted(arg)); - this.on('workflow-saved', (arg) => this.workflowSaved(arg)); - this.on('workflow-pre-execute', (arg) => this.workflowPreExecute(arg)); - this.on('workflow-post-execute', (arg) => this.workflowPostExecute(arg)); - this.on('node-pre-execute', (arg) => this.nodePreExecute(arg)); - this.on('node-post-execute', (arg) => this.nodePostExecute(arg)); - this.on('user-deleted', (arg) => this.userDeleted(arg)); - this.on('user-invited', (arg) => this.userInvited(arg)); - this.on('user-reinvited', (arg) => this.userReinvited(arg)); - this.on('user-updated', (arg) => this.userUpdated(arg)); - this.on('user-signed-up', (arg) => this.userSignedUp(arg)); - this.on('user-logged-in', (arg) => this.userLoggedIn(arg)); - this.on('user-login-failed', (arg) => this.userLoginFailed(arg)); - this.on('user-invite-email-click', (arg) => this.userInviteEmailClick(arg)); - this.on('user-password-reset-email-click', (arg) => this.userPasswordResetEmailClick(arg)); - this.on('user-password-reset-request-click', (arg) => this.userPasswordResetRequestClick(arg)); - this.on('api-key-created', (arg) => this.apiKeyCreated(arg)); - this.on('api-key-deleted', (arg) => this.apiKeyDeleted(arg)); - this.on('email-failed', (arg) => this.emailFailed(arg)); - this.on('credentials-created', (arg) => this.credentialsCreated(arg)); - this.on('credentials-deleted', (arg) => this.credentialsDeleted(arg)); - this.on('credentials-shared', (arg) => this.credentialsShared(arg)); - this.on('credentials-updated', (arg) => this.credentialsUpdated(arg)); - this.on('credentials-deleted', (arg) => this.credentialsDeleted(arg)); - this.on('community-package-installed', (arg) => this.communityPackageInstalled(arg)); - this.on('community-package-updated', (arg) => this.communityPackageUpdated(arg)); - this.on('community-package-deleted', (arg) => this.communityPackageDeleted(arg)); + this.eventRelay.on('workflow-created', (event) => this.workflowCreated(event)); + this.eventRelay.on('workflow-deleted', (event) => this.workflowDeleted(event)); + this.eventRelay.on('workflow-saved', (event) => this.workflowSaved(event)); + this.eventRelay.on('workflow-pre-execute', (event) => this.workflowPreExecute(event)); + this.eventRelay.on('workflow-post-execute', (event) => this.workflowPostExecute(event)); + this.eventRelay.on('node-pre-execute', (event) => this.nodePreExecute(event)); + this.eventRelay.on('node-post-execute', (event) => this.nodePostExecute(event)); + this.eventRelay.on('user-deleted', (event) => this.userDeleted(event)); + this.eventRelay.on('user-invited', (event) => this.userInvited(event)); + this.eventRelay.on('user-reinvited', (event) => this.userReinvited(event)); + this.eventRelay.on('user-updated', (event) => this.userUpdated(event)); + this.eventRelay.on('user-signed-up', (event) => this.userSignedUp(event)); + this.eventRelay.on('user-logged-in', (event) => this.userLoggedIn(event)); + this.eventRelay.on('user-login-failed', (event) => this.userLoginFailed(event)); + this.eventRelay.on('user-invite-email-click', (event) => this.userInviteEmailClick(event)); + this.eventRelay.on('user-password-reset-email-click', (event) => + this.userPasswordResetEmailClick(event), + ); + this.eventRelay.on('user-password-reset-request-click', (event) => + this.userPasswordResetRequestClick(event), + ); + this.eventRelay.on('api-key-created', (event) => this.apiKeyCreated(event)); + this.eventRelay.on('api-key-deleted', (event) => this.apiKeyDeleted(event)); + this.eventRelay.on('email-failed', (event) => this.emailFailed(event)); + this.eventRelay.on('credentials-created', (event) => this.credentialsCreated(event)); + this.eventRelay.on('credentials-deleted', (event) => this.credentialsDeleted(event)); + this.eventRelay.on('credentials-shared', (event) => this.credentialsShared(event)); + this.eventRelay.on('credentials-updated', (event) => this.credentialsUpdated(event)); + this.eventRelay.on('credentials-deleted', (event) => this.credentialsDeleted(event)); + this.eventRelay.on('community-package-installed', (event) => + this.communityPackageInstalled(event), + ); + this.eventRelay.on('community-package-updated', (event) => this.communityPackageUpdated(event)); + this.eventRelay.on('community-package-deleted', (event) => this.communityPackageDeleted(event)); } /** @@ -111,10 +111,10 @@ export class AuditorService extends EventEmitter { }); } - private workflowPostExecute(arg: AuditEventArgs['workflow-post-execute']) { + private workflowPostExecute(event: AuditEventArgs['workflow-post-execute']) { void this.eventBus.sendWorkflowEvent({ eventName: 'n8n.workflow.success', - payload: arg, + payload: event, }); } @@ -209,11 +209,11 @@ export class AuditorService extends EventEmitter { } private userLoginFailed( - arg: AuditEventArgs['user-login-failed'] /* exception: no `UserLike` to redact */, + event: AuditEventArgs['user-login-failed'] /* exception: no `UserLike` to redact */, ) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.login.failed', - payload: arg, + payload: event, }); } @@ -223,10 +223,10 @@ export class AuditorService extends EventEmitter { @Redactable('inviter') @Redactable('invitee') - private userInviteEmailClick(arg: AuditEventArgs['user-invite-email-click']) { + private userInviteEmailClick(event: AuditEventArgs['user-invite-email-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.invitation.accepted', - payload: arg, + payload: event, }); } diff --git a/packages/cli/src/eventbus/event-sender.ts b/packages/cli/src/eventbus/event-relay.service.ts similarity index 51% rename from packages/cli/src/eventbus/event-sender.ts rename to packages/cli/src/eventbus/event-relay.service.ts index 4388394ca6a60..c2b983b32511c 100644 --- a/packages/cli/src/eventbus/event-sender.ts +++ b/packages/cli/src/eventbus/event-relay.service.ts @@ -1,16 +1,18 @@ import { EventEmitter } from 'node:events'; -import type { AuditEventArgs } from './audit.types'; import { Service } from 'typedi'; +import type { AuditEventArgs } from './audit.types'; type EventArgs = AuditEventArgs; // also `TelemetryEventArgs` in future -/** - * Service to send events for other services to listen to. - */ @Service() -export class EventSender extends EventEmitter { - emit(eventName: K, arg: EventArgs[K]) { +export class EventRelay extends EventEmitter { + emit(eventName: K, arg: EventArgs[K]) { super.emit(eventName, arg); return true; } + + on(eventName: K, handler: (arg: EventArgs[K]) => void) { + super.on(eventName, handler); + return this; + } } diff --git a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts index 81dd19c7ca251..fa546759cf705 100644 --- a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts +++ b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts @@ -20,7 +20,7 @@ import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessag import type { EventMessageTypes as EventMessage } from '@/eventbus/EventMessageClasses'; import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { NodeConnectionType } from 'n8n-workflow'; -import type { EventSender } from '@/eventbus/event-sender'; +import type { EventRelay } from '@/eventbus/event-relay.service'; import { mock } from 'jest-mock-extended'; /** @@ -187,7 +187,7 @@ describe('ExecutionRecoveryService', () => { executionRecoveryService = new ExecutionRecoveryService( push, executionRepository, - mock(), + mock(), ); }); diff --git a/packages/cli/src/executions/execution-recovery.service.ts b/packages/cli/src/executions/execution-recovery.service.ts index 7f2a01ce70c20..9692515a058f1 100644 --- a/packages/cli/src/executions/execution-recovery.service.ts +++ b/packages/cli/src/executions/execution-recovery.service.ts @@ -11,7 +11,7 @@ import type { IExecutionResponse } from '@/Interfaces'; import { NodeCrashedError } from '@/errors/node-crashed.error'; import { WorkflowCrashedError } from '@/errors/workflow-crashed.error'; import { ARTIFICIAL_TASK_DATA } from '@/constants'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; /** * Service for recovering key properties in executions. @@ -21,7 +21,7 @@ export class ExecutionRecoveryService { constructor( private readonly push: Push, private readonly executionRepository: ExecutionRepository, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} /** @@ -171,7 +171,7 @@ export class ExecutionRecoveryService { stoppedAt: execution.stoppedAt, status: execution.status, }); - this.eventSender.emit('workflow-post-execute', { + this.eventRelay.emit('workflow-post-execute', { workflowId: execution.workflowData.id, workflowName: execution.workflowData.name, executionId: execution.id, diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index 7749cd3477544..18b45d8b6e4df 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -12,7 +12,7 @@ import { InternalHooks } from '@/InternalHooks'; import { UrlService } from '@/services/url.service'; import type { UserRequest } from '@/requests'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @Service() export class UserService { @@ -21,7 +21,7 @@ export class UserService { private readonly userRepository: UserRepository, private readonly mailer: UserManagementMailer, private readonly urlService: UrlService, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} async update(userId: string, data: Partial) { @@ -158,7 +158,7 @@ export class UserService { email_sent: result.emailSent, invitee_role: role, // same role for all invited users }); - this.eventSender.emit('user-invited', { + this.eventRelay.emit('user-invited', { user: owner, targetUserId: Object.values(toInviteUsers), }); @@ -169,7 +169,7 @@ export class UserService { message_type: 'New user invite', public_api: false, }); - this.eventSender.emit('email-failed', { user: owner, messageType: 'New user invite' }); + this.eventRelay.emit('email-failed', { user: owner, messageType: 'New user invite' }); this.logger.error('Failed to send email', { userId: owner.id, inviteAcceptUrl, diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts index 43a745b9df7da..f08189ffafb74 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts @@ -6,7 +6,6 @@ import url from 'url'; import { Get, Post, RestController, GlobalScope } from '@/decorators'; import { AuthService } from '@/auth/auth.service'; import { AuthenticatedRequest } from '@/requests'; -import { InternalHooks } from '@/InternalHooks'; import querystring from 'querystring'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { AuthError } from '@/errors/response-errors/auth.error'; @@ -28,7 +27,7 @@ import { import { SamlService } from '../saml.service.ee'; import { SamlConfiguration } from '../types/requests'; import { getInitSSOFormView } from '../views/initSsoPost'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController('/sso/saml') export class SamlController { @@ -36,8 +35,7 @@ export class SamlController { private readonly authService: AuthService, private readonly samlService: SamlService, private readonly urlService: UrlService, - private readonly internalHooks: InternalHooks, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} @Get('/metadata', { skipAuth: true }) @@ -128,7 +126,7 @@ export class SamlController { } } if (loginResult.authenticatedUser) { - this.eventSender.emit('user-logged-in', { + this.eventRelay.emit('user-logged-in', { user: loginResult.authenticatedUser, authenticationMethod: 'saml', }); @@ -146,7 +144,7 @@ export class SamlController { return res.status(202).send(loginResult.attributes); } } - this.eventSender.emit('user-login-failed', { + this.eventRelay.emit('user-login-failed', { userEmail: loginResult.attributes.email ?? 'unknown', authenticationMethod: 'saml', }); @@ -155,7 +153,7 @@ export class SamlController { if (isConnectionTestRequest(req)) { return res.send(getSamlConnectionTestFailedView((error as Error).message)); } - this.eventSender.emit('user-login-failed', { + this.eventRelay.emit('user-login-failed', { userEmail: 'unknown', authenticationMethod: 'saml', }); diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 44b5dc273c3b9..d6ceb070da2de 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -32,7 +32,7 @@ import type { Scope } from '@n8n/permissions'; import type { EntityManager } from '@n8n/typeorm'; import { In } from '@n8n/typeorm'; import { SharedWorkflow } from '@/databases/entities/SharedWorkflow'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @Service() export class WorkflowService { @@ -52,7 +52,7 @@ export class WorkflowService { private readonly workflowSharingService: WorkflowSharingService, private readonly projectService: ProjectService, private readonly executionRepository: ExecutionRepository, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) { @@ -218,7 +218,7 @@ export class WorkflowService { await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]); void Container.get(InternalHooks).onWorkflowSaved(user, updatedWorkflow, false); - this.eventSender.emit('workflow-saved', { + this.eventRelay.emit('workflow-saved', { user, workflowId: updatedWorkflow.id, workflowName: updatedWorkflow.name, @@ -281,7 +281,7 @@ export class WorkflowService { await this.binaryDataService.deleteMany(idsForDeletion); void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false); - this.eventSender.emit('workflow-deleted', { user, workflowId }); + this.eventRelay.emit('workflow-deleted', { user, workflowId }); await this.externalHooks.run('workflow.afterDelete', [workflowId]); return workflow; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 40d8c03bf6e57..1d551ea03c575 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -41,7 +41,7 @@ import { In, type FindOptionsRelations } from '@n8n/typeorm'; import type { Project } from '@/databases/entities/Project'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import { z } from 'zod'; -import { EventSender } from '@/eventbus/event-sender'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController('/workflows') export class WorkflowsController { @@ -65,7 +65,7 @@ export class WorkflowsController { private readonly projectRepository: ProjectRepository, private readonly projectService: ProjectService, private readonly projectRelationRepository: ProjectRelationRepository, - private readonly eventSender: EventSender, + private readonly eventRelay: EventRelay, ) {} @Post('/') @@ -177,7 +177,7 @@ export class WorkflowsController { await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]); void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false); - this.eventSender.emit('workflow-created', { user: req.user, workflow: newWorkflow }); + this.eventRelay.emit('workflow-created', { user: req.user, workflow: newWorkflow }); const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id); From 993e815a77014961d4b29da8d7bbeccedeca4347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 17 Jun 2024 16:01:23 +0200 Subject: [PATCH 20/25] Add example --- packages/cli/src/decorators/Redactable.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts index 117ea8502891c..2b444abecd78c 100644 --- a/packages/cli/src/decorators/Redactable.ts +++ b/packages/cli/src/decorators/Redactable.ts @@ -13,9 +13,15 @@ function toRedactable(userLike: UserLike) { type FieldName = 'user' | 'inviter' | 'invitee'; /** - * Mark redactable properties in a `UserLike` field in an `AuditorService` + * Mark redactable properties in a `UserLike` field in an `AuditEventRelay` * method arg. These properties will be later redacted by the log streaming - * destination based on user prefs. Relevant only for `n8n.audit.*` logs. + * destination based on user prefs. Only for `n8n.audit.*` logs. + * + * @example + * + * `{ id: '123'; email: 'test@example.com', role: 'some-role' }` -> + * `{ userId: '123'; _email: 'test@example.com', globalRole: 'some-role' }` + * ``` */ export const Redactable = (fieldName: FieldName = 'user'): MethodDecorator => From b0b0d777a22aae51a8ae42201593075472ed39eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 17 Jun 2024 16:08:17 +0200 Subject: [PATCH 21/25] Introduce `RedactableError` --- packages/cli/src/decorators/Redactable.ts | 3 +++ packages/cli/src/errors/redactable.error.ts | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 packages/cli/src/errors/redactable.error.ts diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts index 2b444abecd78c..2b4716407dba7 100644 --- a/packages/cli/src/decorators/Redactable.ts +++ b/packages/cli/src/decorators/Redactable.ts @@ -1,3 +1,4 @@ +import { RedactableError } from '@/errors/redactable.error'; import type { UserLike } from '@/eventbus/audit.types'; function toRedactable(userLike: UserLike) { @@ -33,6 +34,8 @@ export const Redactable = propertyDescriptor.value = function (...args: MethodArgs) { const index = args.findIndex((arg) => arg[fieldName] !== undefined); + if (index === -1) throw new RedactableError(fieldName, args.toString()); + const userLike = args[index]?.[fieldName]; // @ts-expect-error Transformation diff --git a/packages/cli/src/errors/redactable.error.ts b/packages/cli/src/errors/redactable.error.ts new file mode 100644 index 0000000000000..1d40b1e07efab --- /dev/null +++ b/packages/cli/src/errors/redactable.error.ts @@ -0,0 +1,10 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class RedactableError extends ApplicationError { + constructor(fieldName: string, args: string) { + super( + `Failed to find "${fieldName}" property in argument "${args.toString()}". Please set the decorator \`@Redactable()\` only on \`AuditEventRelay\` methods where the argument contains a "${fieldName}" property.`, + { level: 'warning' }, + ); + } +} From 5fe38f45033363a42e2cc5849002a393d944bfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 17 Jun 2024 16:22:13 +0200 Subject: [PATCH 22/25] Generalize events --- packages/cli/src/decorators/Redactable.ts | 11 ++-- .../audit-event-relay.service.test.ts | 6 +- .../src/eventbus/audit-event-relay.service.ts | 65 ++++++++----------- .../cli/src/eventbus/event-relay.service.ts | 8 +-- .../{audit.types.ts => event.types.ts} | 5 +- 5 files changed, 44 insertions(+), 51 deletions(-) rename packages/cli/src/eventbus/{audit.types.ts => event.types.ts} (96%) diff --git a/packages/cli/src/decorators/Redactable.ts b/packages/cli/src/decorators/Redactable.ts index 2b4716407dba7..e5debeb7a1507 100644 --- a/packages/cli/src/decorators/Redactable.ts +++ b/packages/cli/src/decorators/Redactable.ts @@ -1,5 +1,5 @@ import { RedactableError } from '@/errors/redactable.error'; -import type { UserLike } from '@/eventbus/audit.types'; +import type { UserLike } from '@/eventbus/event.types'; function toRedactable(userLike: UserLike) { return { @@ -14,15 +14,16 @@ function toRedactable(userLike: UserLike) { type FieldName = 'user' | 'inviter' | 'invitee'; /** - * Mark redactable properties in a `UserLike` field in an `AuditEventRelay` + * Mark redactable properties in a `{ user: UserLike }` field in an `AuditEventRelay` * method arg. These properties will be later redacted by the log streaming * destination based on user prefs. Only for `n8n.audit.*` logs. * + * Also transform `id` to `userId` and `role` to `globalRole`. + * * @example * - * `{ id: '123'; email: 'test@example.com', role: 'some-role' }` -> - * `{ userId: '123'; _email: 'test@example.com', globalRole: 'some-role' }` - * ``` + * { id: '123'; email: 'test@example.com', role: 'some-role' } -> + * { userId: '123'; _email: 'test@example.com', globalRole: 'some-role' } */ export const Redactable = (fieldName: FieldName = 'user'): MethodDecorator => diff --git a/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts index 3bace4c0a4348..84408e56a711c 100644 --- a/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts +++ b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts @@ -1,7 +1,7 @@ import { mock } from 'jest-mock-extended'; import { AuditEventRelay } from '../audit-event-relay.service'; import type { MessageEventBus } from '../MessageEventBus/MessageEventBus'; -import type { AuditEventArgs } from '../audit.types'; +import type { Event } from '../event.types'; import type { EventRelay } from '../event-relay.service'; describe('AuditorService', () => { @@ -14,7 +14,7 @@ describe('AuditorService', () => { }); it('should handle `user-deleted` event', () => { - const arg: AuditEventArgs['user-deleted'] = { + const arg: Event['user-deleted'] = { user: { id: '123', email: 'john@n8n.io', @@ -40,7 +40,7 @@ describe('AuditorService', () => { }); it('should handle `user-invite-email-click` event', () => { - const arg: AuditEventArgs['user-invite-email-click'] = { + const arg: Event['user-invite-email-click'] = { inviter: { id: '123', email: 'john@n8n.io', diff --git a/packages/cli/src/eventbus/audit-event-relay.service.ts b/packages/cli/src/eventbus/audit-event-relay.service.ts index 4fac8e636ac68..e4a9632a6cc3f 100644 --- a/packages/cli/src/eventbus/audit-event-relay.service.ts +++ b/packages/cli/src/eventbus/audit-event-relay.service.ts @@ -2,7 +2,7 @@ import { Service } from 'typedi'; import { MessageEventBus } from './MessageEventBus/MessageEventBus'; import { Redactable } from '@/decorators/Redactable'; import { EventRelay } from './event-relay.service'; -import type { AuditEventArgs } from './audit.types'; +import type { Event } from './event.types'; import type { IWorkflowBase } from 'n8n-workflow'; @Service() @@ -56,7 +56,7 @@ export class AuditEventRelay { */ @Redactable() - private workflowCreated({ user, workflow }: AuditEventArgs['workflow-created']) { + private workflowCreated({ user, workflow }: Event['workflow-created']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.created', payload: { @@ -68,7 +68,7 @@ export class AuditEventRelay { } @Redactable() - private workflowDeleted({ user, workflowId }: AuditEventArgs['workflow-deleted']) { + private workflowDeleted({ user, workflowId }: Event['workflow-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.deleted', payload: { ...user, workflowId }, @@ -76,7 +76,7 @@ export class AuditEventRelay { } @Redactable() - private workflowSaved({ user, workflowId, workflowName }: AuditEventArgs['workflow-saved']) { + private workflowSaved({ user, workflowId, workflowName }: Event['workflow-saved']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.workflow.updated', payload: { @@ -87,7 +87,7 @@ export class AuditEventRelay { }); } - private workflowPreExecute({ data, executionId }: AuditEventArgs['workflow-pre-execute']) { + private workflowPreExecute({ data, executionId }: Event['workflow-pre-execute']) { const payload = 'executionData' in data ? { @@ -111,7 +111,7 @@ export class AuditEventRelay { }); } - private workflowPostExecute(event: AuditEventArgs['workflow-post-execute']) { + private workflowPostExecute(event: Event['workflow-post-execute']) { void this.eventBus.sendWorkflowEvent({ eventName: 'n8n.workflow.success', payload: event, @@ -122,7 +122,7 @@ export class AuditEventRelay { * Node */ - private nodePreExecute({ workflow, executionId, nodeName }: AuditEventArgs['node-pre-execute']) { + private nodePreExecute({ workflow, executionId, nodeName }: Event['node-pre-execute']) { void this.eventBus.sendNodeEvent({ eventName: 'n8n.node.started', payload: { @@ -135,11 +135,7 @@ export class AuditEventRelay { }); } - private nodePostExecute({ - workflow, - executionId, - nodeName, - }: AuditEventArgs['node-post-execute']) { + private nodePostExecute({ workflow, executionId, nodeName }: Event['node-post-execute']) { void this.eventBus.sendNodeEvent({ eventName: 'n8n.node.finished', payload: { @@ -157,7 +153,7 @@ export class AuditEventRelay { */ @Redactable() - private userDeleted({ user }: AuditEventArgs['user-deleted']) { + private userDeleted({ user }: Event['user-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.deleted', payload: user, @@ -165,7 +161,7 @@ export class AuditEventRelay { } @Redactable() - private userInvited({ user, targetUserId }: AuditEventArgs['user-invited']) { + private userInvited({ user, targetUserId }: Event['user-invited']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.invited', payload: { ...user, targetUserId }, @@ -173,7 +169,7 @@ export class AuditEventRelay { } @Redactable() - private userReinvited({ user, targetUserId }: AuditEventArgs['user-reinvited']) { + private userReinvited({ user, targetUserId }: Event['user-reinvited']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reinvited', payload: { ...user, targetUserId }, @@ -181,7 +177,7 @@ export class AuditEventRelay { } @Redactable() - private userUpdated({ user, fieldsChanged }: AuditEventArgs['user-updated']) { + private userUpdated({ user, fieldsChanged }: Event['user-updated']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.updated', payload: { ...user, fieldsChanged }, @@ -193,7 +189,7 @@ export class AuditEventRelay { */ @Redactable() - private userSignedUp({ user }: AuditEventArgs['user-signed-up']) { + private userSignedUp({ user }: Event['user-signed-up']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.signedup', payload: user, @@ -201,7 +197,7 @@ export class AuditEventRelay { } @Redactable() - private userLoggedIn({ user, authenticationMethod }: AuditEventArgs['user-logged-in']) { + private userLoggedIn({ user, authenticationMethod }: Event['user-logged-in']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.login.success', payload: { ...user, authenticationMethod }, @@ -209,7 +205,7 @@ export class AuditEventRelay { } private userLoginFailed( - event: AuditEventArgs['user-login-failed'] /* exception: no `UserLike` to redact */, + event: Event['user-login-failed'] /* exception: no `UserLike` to redact */, ) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.login.failed', @@ -223,7 +219,7 @@ export class AuditEventRelay { @Redactable('inviter') @Redactable('invitee') - private userInviteEmailClick(event: AuditEventArgs['user-invite-email-click']) { + private userInviteEmailClick(event: Event['user-invite-email-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.invitation.accepted', payload: event, @@ -231,7 +227,7 @@ export class AuditEventRelay { } @Redactable() - private userPasswordResetEmailClick({ user }: AuditEventArgs['user-password-reset-email-click']) { + private userPasswordResetEmailClick({ user }: Event['user-password-reset-email-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reset', payload: user, @@ -239,9 +235,7 @@ export class AuditEventRelay { } @Redactable() - private userPasswordResetRequestClick({ - user, - }: AuditEventArgs['user-password-reset-request-click']) { + private userPasswordResetRequestClick({ user }: Event['user-password-reset-request-click']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.reset.requested', payload: user, @@ -253,7 +247,7 @@ export class AuditEventRelay { */ @Redactable() - private apiKeyCreated({ user }: AuditEventArgs['api-key-created']) { + private apiKeyCreated({ user }: Event['api-key-created']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.api.created', payload: user, @@ -261,7 +255,7 @@ export class AuditEventRelay { } @Redactable() - private apiKeyDeleted({ user }: AuditEventArgs['api-key-deleted']) { + private apiKeyDeleted({ user }: Event['api-key-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.api.deleted', payload: user, @@ -273,7 +267,7 @@ export class AuditEventRelay { */ @Redactable() - private emailFailed({ user, messageType }: AuditEventArgs['email-failed']) { + private emailFailed({ user, messageType }: Event['email-failed']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.email.failed', payload: { ...user, messageType }, @@ -285,7 +279,7 @@ export class AuditEventRelay { */ @Redactable() - private credentialsCreated({ user, ...rest }: AuditEventArgs['credentials-created']) { + private credentialsCreated({ user, ...rest }: Event['credentials-created']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.created', payload: { ...user, ...rest }, @@ -293,7 +287,7 @@ export class AuditEventRelay { } @Redactable() - private credentialsDeleted({ user, ...rest }: AuditEventArgs['credentials-deleted']) { + private credentialsDeleted({ user, ...rest }: Event['credentials-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.deleted', payload: { ...user, ...rest }, @@ -301,7 +295,7 @@ export class AuditEventRelay { } @Redactable() - private credentialsShared({ user, ...rest }: AuditEventArgs['credentials-shared']) { + private credentialsShared({ user, ...rest }: Event['credentials-shared']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.shared', payload: { ...user, ...rest }, @@ -309,7 +303,7 @@ export class AuditEventRelay { } @Redactable() - private credentialsUpdated({ user, ...rest }: AuditEventArgs['credentials-updated']) { + private credentialsUpdated({ user, ...rest }: Event['credentials-updated']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.user.credentials.updated', payload: { ...user, ...rest }, @@ -321,10 +315,7 @@ export class AuditEventRelay { */ @Redactable() - private communityPackageInstalled({ - user, - ...rest - }: AuditEventArgs['community-package-installed']) { + private communityPackageInstalled({ user, ...rest }: Event['community-package-installed']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.package.installed', payload: { ...user, ...rest }, @@ -332,7 +323,7 @@ export class AuditEventRelay { } @Redactable() - private communityPackageUpdated({ user, ...rest }: AuditEventArgs['community-package-updated']) { + private communityPackageUpdated({ user, ...rest }: Event['community-package-updated']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.package.updated', payload: { ...user, ...rest }, @@ -340,7 +331,7 @@ export class AuditEventRelay { } @Redactable() - private communityPackageDeleted({ user, ...rest }: AuditEventArgs['community-package-deleted']) { + private communityPackageDeleted({ user, ...rest }: Event['community-package-deleted']) { void this.eventBus.sendAuditEvent({ eventName: 'n8n.audit.package.deleted', payload: { ...user, ...rest }, diff --git a/packages/cli/src/eventbus/event-relay.service.ts b/packages/cli/src/eventbus/event-relay.service.ts index c2b983b32511c..8f6bb4c5c1420 100644 --- a/packages/cli/src/eventbus/event-relay.service.ts +++ b/packages/cli/src/eventbus/event-relay.service.ts @@ -1,17 +1,15 @@ import { EventEmitter } from 'node:events'; import { Service } from 'typedi'; -import type { AuditEventArgs } from './audit.types'; - -type EventArgs = AuditEventArgs; // also `TelemetryEventArgs` in future +import type { Event } from './event.types'; @Service() export class EventRelay extends EventEmitter { - emit(eventName: K, arg: EventArgs[K]) { + emit(eventName: K, arg: Event[K]) { super.emit(eventName, arg); return true; } - on(eventName: K, handler: (arg: EventArgs[K]) => void) { + on(eventName: K, handler: (arg: Event[K]) => void) { super.on(eventName, handler); return this; } diff --git a/packages/cli/src/eventbus/audit.types.ts b/packages/cli/src/eventbus/event.types.ts similarity index 96% rename from packages/cli/src/eventbus/audit.types.ts rename to packages/cli/src/eventbus/event.types.ts index e58c81765e436..b4162f0333736 100644 --- a/packages/cli/src/eventbus/audit.types.ts +++ b/packages/cli/src/eventbus/event.types.ts @@ -9,7 +9,10 @@ export type UserLike = { role: string; }; -export type AuditEventArgs = { +/** + * Events sent by services and consumed by relays, e.g. `AuditEventRelay`. + */ +export type Event = { 'workflow-created': { user: UserLike; workflow: IWorkflowBase; From 1c3d9cbcf2d4e2ca886143e0f35bce3134201869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 18 Jun 2024 10:07:00 +0200 Subject: [PATCH 23/25] Add missing property `reason` --- packages/cli/src/controllers/auth.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index a0dbac374da58..f5208c868b0ce 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -103,6 +103,7 @@ export class AuthController { this.eventRelay.emit('user-login-failed', { authenticationMethod: usedAuthenticationMethod, userEmail: email, + reason: 'wrong credentials', }); throw new AuthError('Wrong username or password. Do you have caps lock on?'); } From ac9d0e81b399731e2d25099c10d80813f4c67088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 18 Jun 2024 15:29:52 +0200 Subject: [PATCH 24/25] Unsilence `RedactableError` --- packages/cli/src/errors/redactable.error.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/errors/redactable.error.ts b/packages/cli/src/errors/redactable.error.ts index 1d40b1e07efab..0f6697a0652d6 100644 --- a/packages/cli/src/errors/redactable.error.ts +++ b/packages/cli/src/errors/redactable.error.ts @@ -4,7 +4,6 @@ export class RedactableError extends ApplicationError { constructor(fieldName: string, args: string) { super( `Failed to find "${fieldName}" property in argument "${args.toString()}". Please set the decorator \`@Redactable()\` only on \`AuditEventRelay\` methods where the argument contains a "${fieldName}" property.`, - { level: 'warning' }, ); } } From 5e7f3ecd8d9aa0f7d0920b64bb2b05cb40b7e53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 19 Jun 2024 17:57:06 +0200 Subject: [PATCH 25/25] Restore event bus dep temporarily --- packages/cli/src/InternalHooks.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 2b6d7178aac4e..f8c56be0d43bf 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -32,6 +32,7 @@ import type { Project } from '@db/entities/Project'; import type { ProjectRole } from '@db/entities/ProjectRelation'; import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository'; import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository'; +import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus'; @Service() export class InternalHooks { @@ -45,6 +46,7 @@ export class InternalHooks { private readonly license: License, private readonly projectRelationRepository: ProjectRelationRepository, private readonly sharedCredentialsRepository: SharedCredentialsRepository, + private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry ) { eventsService.on( 'telemetry.onFirstProductionWorkflowSuccess',