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.
This commit is contained in:
martmull
2026-05-08 12:03:39 +02:00
committed by GitHub
parent 837a946b5f
commit 0d05788547
5 changed files with 29 additions and 2 deletions
@@ -84,10 +84,14 @@ export class LogicFunctionExecutorService {
logicFunctionId,
workspaceId,
payload,
userId,
userWorkspaceId,
}: {
logicFunctionId: string;
workspaceId: string;
payload: object;
userId?: string;
userWorkspaceId?: string;
}): Promise<LogicFunctionExecuteResult> {
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'));
@@ -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,
}),
),
);
@@ -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) {