Add WorkspaceAuthContextMiddleware (#17487)
## Context
Introduces a middleware that automatically sets the workspace auth
context in AsyncLocalStorage for HTTP requests, making it available
throughout the request lifecycle without explicit parameter passing.
The motivation behind this change is to reduce boilerplate and simplify
the developer experience when working with workspace data in HTTP
request handlers.
The Problem (Before)
Every HTTP request handler that needed to access workspace data had to:
- Extract auth-related info from decorators (@AuthWorkspace(),
@AuthUserWorkspaceId(), etc.) in controller/resolver and pass down to
services
- Build or pass the authContext explicitly (sometimes with type
assertion which was flaky)
Then call executeInWorkspaceContext(authContext, async () => { ... })
## Changes
- Add WorkspaceAuthContextMiddleware that extracts auth context from the
request and stores it in AsyncLocalStorage
- Register middleware for GraphQL, metadata, and REST routes (runs after
hydration middlewares)
- Simplify executeInWorkspaceContext signature: fn is now the first
parameter, authContext is optional second
- If authContext is not provided, it's automatically retrieved from the
storage (set by middleware)
- Update all callers (~120 files) to use the new parameter order
- Fixes a bug in search where system auth context was used, bypassing
RLS feature.
This commit is contained in:
+28
-31
@@ -36,37 +36,34 @@ export class WorkflowCreateManyPostQueryHook
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext as WorkspaceAuthContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workflowVersion',
|
||||
);
|
||||
|
||||
const position = await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: {
|
||||
isCustom: false,
|
||||
nameSingular: 'workflowVersion',
|
||||
},
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const workflowVersionsToCreate = payload.map((workflow) => ({
|
||||
workflowId: workflow.id,
|
||||
status: WorkflowVersionStatus.DRAFT,
|
||||
name: 'v1',
|
||||
position,
|
||||
}));
|
||||
|
||||
await Promise.all(
|
||||
workflowVersionsToCreate.map((workflowVersion) => {
|
||||
return workflowVersionRepository.insert(workflowVersion);
|
||||
}),
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workflowVersion',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const position = await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: {
|
||||
isCustom: false,
|
||||
nameSingular: 'workflowVersion',
|
||||
},
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const workflowVersionsToCreate = payload.map((workflow) => ({
|
||||
workflowId: workflow.id,
|
||||
status: WorkflowVersionStatus.DRAFT,
|
||||
name: 'v1',
|
||||
position,
|
||||
}));
|
||||
|
||||
await Promise.all(
|
||||
workflowVersionsToCreate.map((workflowVersion) => {
|
||||
return workflowVersionRepository.insert(workflowVersion);
|
||||
}),
|
||||
);
|
||||
}, authContext as WorkspaceAuthContext);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-24
@@ -38,31 +38,28 @@ export class WorkflowCreateOnePostQueryHook
|
||||
|
||||
const workflow = payload[0];
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext as WorkspaceAuthContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workflowVersion',
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workflowVersion',
|
||||
);
|
||||
|
||||
const position = await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: {
|
||||
isCustom: false,
|
||||
nameSingular: 'workflowVersion',
|
||||
},
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
const position = await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: {
|
||||
isCustom: false,
|
||||
nameSingular: 'workflowVersion',
|
||||
},
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
await workflowVersionRepository.insert({
|
||||
workflowId: workflow.id,
|
||||
status: WorkflowVersionStatus.DRAFT,
|
||||
name: 'v1',
|
||||
position,
|
||||
});
|
||||
},
|
||||
);
|
||||
await workflowVersionRepository.insert({
|
||||
workflowId: workflow.id,
|
||||
status: WorkflowVersionStatus.DRAFT,
|
||||
name: 'v1',
|
||||
position,
|
||||
});
|
||||
}, authContext as WorkspaceAuthContext);
|
||||
}
|
||||
}
|
||||
|
||||
+60
-63
@@ -62,7 +62,6 @@ export class WorkflowCommonWorkspaceService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
@@ -79,6 +78,7 @@ export class WorkflowCommonWorkspaceService {
|
||||
|
||||
return this.getValidWorkflowVersionOrFail(workflowVersion);
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,78 +165,75 @@ export class WorkflowCommonWorkspaceService {
|
||||
}): Promise<void> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRunRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
const workflowRunRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowAutomatedTrigger',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowAutomatedTrigger',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
for (const workflowId of workflowIds) {
|
||||
switch (operation) {
|
||||
case 'delete':
|
||||
await workflowAutomatedTriggerRepository.softDelete({
|
||||
workflowId,
|
||||
});
|
||||
for (const workflowId of workflowIds) {
|
||||
switch (operation) {
|
||||
case 'delete':
|
||||
await workflowAutomatedTriggerRepository.softDelete({
|
||||
workflowId,
|
||||
});
|
||||
|
||||
await workflowRunRepository.softDelete({
|
||||
workflowId,
|
||||
});
|
||||
await workflowRunRepository.softDelete({
|
||||
workflowId,
|
||||
});
|
||||
|
||||
await workflowVersionRepository.softDelete({
|
||||
workflowId,
|
||||
});
|
||||
await workflowVersionRepository.softDelete({
|
||||
workflowId,
|
||||
});
|
||||
|
||||
break;
|
||||
case 'restore':
|
||||
await workflowAutomatedTriggerRepository.restore({
|
||||
workflowId,
|
||||
});
|
||||
break;
|
||||
case 'restore':
|
||||
await workflowAutomatedTriggerRepository.restore({
|
||||
workflowId,
|
||||
});
|
||||
|
||||
await workflowRunRepository.restore({
|
||||
workflowId,
|
||||
});
|
||||
await workflowRunRepository.restore({
|
||||
workflowId,
|
||||
});
|
||||
|
||||
await workflowVersionRepository.restore({
|
||||
workflowId,
|
||||
});
|
||||
await workflowVersionRepository.restore({
|
||||
workflowId,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
await this.deactivateVersionOnDelete({
|
||||
workflowVersionRepository,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
operation,
|
||||
});
|
||||
|
||||
await this.handleServerlessFunctionSubEntities({
|
||||
workflowVersionRepository,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
operation,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await this.deactivateVersionOnDelete({
|
||||
workflowVersionRepository,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
operation,
|
||||
});
|
||||
|
||||
await this.handleServerlessFunctionSubEntities({
|
||||
workflowVersionRepository,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
operation,
|
||||
});
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
private async deactivateVersionOnDelete({
|
||||
|
||||
+51
-56
@@ -48,36 +48,33 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowAlreadyHasDraftVersion =
|
||||
await workflowVersionRepository.exists({
|
||||
where: {
|
||||
workflowId: payload.data.workflowId,
|
||||
status: WorkflowVersionStatus.DRAFT,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
const workflowAlreadyHasDraftVersion =
|
||||
await workflowVersionRepository.exists({
|
||||
where: {
|
||||
workflowId: payload.data.workflowId,
|
||||
status: WorkflowVersionStatus.DRAFT,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
if (workflowAlreadyHasDraftVersion) {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Cannot create multiple draft versions for the same workflow',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: msg`Cannot create multiple draft versions for the same workflow`,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (workflowAlreadyHasDraftVersion) {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Cannot create multiple draft versions for the same workflow',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: msg`Cannot create multiple draft versions for the same workflow`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
async validateWorkflowVersionForUpdateOne({
|
||||
@@ -130,35 +127,33 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const otherWorkflowVersionsExist =
|
||||
await workflowVersionRepository.exists({
|
||||
where: {
|
||||
workflowId: workflowVersion.workflowId,
|
||||
deletedAt: IsNull(),
|
||||
id: Not(workflowVersion.id),
|
||||
},
|
||||
});
|
||||
const otherWorkflowVersionsExist = await workflowVersionRepository.exists(
|
||||
{
|
||||
where: {
|
||||
workflowId: workflowVersion.workflowId,
|
||||
deletedAt: IsNull(),
|
||||
id: Not(workflowVersion.id),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!otherWorkflowVersionsExist) {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'The initial version of a workflow can not be deleted',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: msg`The initial version of a workflow can not be deleted`,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (!otherWorkflowVersionsExist) {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'The initial version of a workflow can not be deleted',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: msg`The initial version of a workflow can not be deleted`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user