From 0d057885473342725b4fccd92ac80b6b347ad88b Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 8 May 2026 12:03:39 +0200 Subject: [PATCH] Protect sendEmail endpoint and thread user context through logic function executor (#20369) - Thread userId and userWorkspaceId through LogicFunctionExecutorService.execute() so application access tokens carry user context when available. This allows logic functions triggered by authenticated HTTP routes to call sendEmail with proper user identity, making the existing verifyOwnership() check work naturally. - Gate the sendEmail resolver with SettingsPermissionGuard(PermissionFlagType.SEND_EMAIL_TOOL) instead of NoPermissionGuard, ensuring only callers with the SEND_EMAIL_TOOL permission can send emails. --- .../logic-function-executor.service.ts | 12 ++++++++++++ .../jobs/logic-function-trigger.job.ts | 4 ++++ .../triggers/route/route-trigger.service.ts | 4 ++++ .../resolvers/send-email.resolver.ts | 9 +++++++-- .../message-outbound-manager/send-email.module.ts | 2 ++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts index d05c543d96..6af78e674e 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts @@ -84,10 +84,14 @@ export class LogicFunctionExecutorService { logicFunctionId, workspaceId, payload, + userId, + userWorkspaceId, }: { logicFunctionId: string; workspaceId: string; payload: object; + userId?: string; + userWorkspaceId?: string; }): Promise { await this.throttleExecution(workspaceId); @@ -101,6 +105,8 @@ export class LogicFunctionExecutorService { workspaceId, flatApplication, flatApplicationVariables, + userId, + userWorkspaceId, }); const driver = this.logicFunctionDriverFactory.getCurrentDriver(); @@ -224,15 +230,21 @@ export class LogicFunctionExecutorService { workspaceId, flatApplication, flatApplicationVariables, + userId, + userWorkspaceId, }: { workspaceId: string; flatApplication: FlatApplication; flatApplicationVariables: FlatApplicationVariable[]; + userId?: string; + userWorkspaceId?: string; }) { const applicationAccessToken = await this.applicationTokenService.generateApplicationAccessToken({ workspaceId, applicationId: flatApplication.id, + userId, + userWorkspaceId, }); const baseUrl = cleanServerUrl(this.twentyConfigService.get('SERVER_URL')); diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts index a4d590caa8..901bdb64b7 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts @@ -9,6 +9,8 @@ export type LogicFunctionTriggerJobData = { logicFunctionId: string; workspaceId: string; payload?: object; + userId?: string; + userWorkspaceId?: string; }; @Processor({ @@ -29,6 +31,8 @@ export class LogicFunctionTriggerJob { logicFunctionId: logicFunctionPayload.logicFunctionId, workspaceId: logicFunctionPayload.workspaceId, payload: logicFunctionPayload.payload ?? {}, + userId: logicFunctionPayload.userId, + userWorkspaceId: logicFunctionPayload.userWorkspaceId, }), ), ); diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts index daf98cbc07..9c674895ef 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts @@ -167,6 +167,7 @@ export class RouteTriggerService { const httpRouteSettings = logicFunction.httpRouteTriggerSettings; let userWorkspaceId: string | null = null; + let userId: string | null = null; if (httpRouteSettings?.isAuthRequired) { const authContext = await this.validateWorkspaceFromRequest({ @@ -175,6 +176,7 @@ export class RouteTriggerService { }); userWorkspaceId = authContext.userWorkspaceId ?? null; + userId = authContext.user?.id ?? null; } const event = buildLogicFunctionEvent({ @@ -191,6 +193,8 @@ export class RouteTriggerService { logicFunctionId: logicFunction.id, workspaceId: logicFunction.workspaceId, payload: event, + ...(userId ? { userId } : {}), + ...(userWorkspaceId ? { userWorkspaceId } : {}), }); } catch (error) { if (error instanceof RouteTriggerException) { diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts index 9d839cc77c..19bd41d51d 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts @@ -9,6 +9,8 @@ import { Args, Mutation } from '@nestjs/graphql'; import { FileFolder } from 'twenty-shared/types'; +import { PermissionFlagType } from 'twenty-shared/constants'; + import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; import { FileEmailAttachmentService } from 'src/engine/core-modules/file/file-email-attachment/services/file-email-attachment.service'; @@ -17,7 +19,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service'; import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; +import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; import { SendEmailOutputDTO } from 'src/modules/messaging/message-outbound-manager/dtos/send-email-output.dto'; @@ -27,7 +29,10 @@ import { SendEmailService } from 'src/modules/messaging/message-outbound-manager @MetadataResolver() @UsePipes(ResolverValidationPipe) @UseFilters(AuthGraphqlApiExceptionFilter) -@UseGuards(WorkspaceAuthGuard, NoPermissionGuard) +@UseGuards( + WorkspaceAuthGuard, + SettingsPermissionGuard(PermissionFlagType.SEND_EMAIL_TOOL), +) export class SendEmailResolver { private readonly logger = new Logger(SendEmailResolver.name); diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/send-email.module.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/send-email.module.ts index 2f62555149..4299c2f28c 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/send-email.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/send-email.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { FileEmailAttachmentModule } from 'src/engine/core-modules/file/file-email-attachment/file-email-attachment.module'; import { ToolModule } from 'src/engine/core-modules/tool/tool.module'; import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { SendEmailResolver } from 'src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver'; import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbound-manager/messaging-send-manager.module'; @@ -12,6 +13,7 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou ToolModule, MessagingSendManagerModule, ConnectedAccountMetadataModule, + PermissionsModule, ], providers: [SendEmailResolver], })