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:
+20
-26
@@ -27,22 +27,19 @@ export class AutomatedTriggerWorkspaceService {
|
||||
}) {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowAutomatedTrigger',
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowAutomatedTrigger',
|
||||
);
|
||||
|
||||
await workflowAutomatedTriggerRepository.insert({
|
||||
type,
|
||||
settings,
|
||||
workflowId,
|
||||
});
|
||||
},
|
||||
);
|
||||
await workflowAutomatedTriggerRepository.insert({
|
||||
type,
|
||||
settings,
|
||||
workflowId,
|
||||
});
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
async deleteAutomatedTrigger({
|
||||
@@ -54,17 +51,14 @@ export class AutomatedTriggerWorkspaceService {
|
||||
}) {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowAutomatedTrigger',
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowAutomatedTrigger',
|
||||
);
|
||||
|
||||
await workflowAutomatedTriggerRepository.delete({ workflowId });
|
||||
},
|
||||
);
|
||||
await workflowAutomatedTriggerRepository.delete({ workflowId });
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
getRepository: jest.fn().mockResolvedValue(mockRepository),
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
.mockImplementation((_authContext: any, fn: () => any) => fn()),
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
} as any;
|
||||
|
||||
messageQueueService = {
|
||||
|
||||
+86
-93
@@ -247,64 +247,60 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
}) {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const { fieldIdByJoinColumnName } =
|
||||
buildFieldMapsFromFlatObjectMetadata(
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const { fieldIdByJoinColumnName } = buildFieldMapsFromFlatObjectMetadata(
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
);
|
||||
|
||||
for (const [joinColumnName, joinFieldId] of Object.entries(
|
||||
fieldIdByJoinColumnName,
|
||||
)) {
|
||||
const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: joinFieldId,
|
||||
});
|
||||
|
||||
const joinRecordIds = records
|
||||
.map((record) => record[joinColumnName])
|
||||
.filter(isDefined);
|
||||
|
||||
if (joinRecordIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedObjectMetadataId =
|
||||
joinField.relationTargetObjectMetadataId;
|
||||
|
||||
if (!isDefined(relatedObjectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedObjectMetadataNameSingular =
|
||||
flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
|
||||
|
||||
if (!isDefined(relatedObjectMetadataNameSingular)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedObjectRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
relatedObjectMetadataNameSingular,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
for (const [joinColumnName, joinFieldId] of Object.entries(
|
||||
fieldIdByJoinColumnName,
|
||||
)) {
|
||||
const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: joinFieldId,
|
||||
});
|
||||
const relatedRecords = await relatedObjectRepository.find({
|
||||
where: { id: In(joinRecordIds) },
|
||||
});
|
||||
|
||||
const joinRecordIds = records
|
||||
.map((record) => record[joinColumnName])
|
||||
.filter(isDefined);
|
||||
|
||||
if (joinRecordIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedObjectMetadataId =
|
||||
joinField.relationTargetObjectMetadataId;
|
||||
|
||||
if (!isDefined(relatedObjectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedObjectMetadataNameSingular =
|
||||
flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
|
||||
|
||||
if (!isDefined(relatedObjectMetadataNameSingular)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedObjectRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
relatedObjectMetadataNameSingular,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const relatedRecords = await relatedObjectRepository.find({
|
||||
where: { id: In(joinRecordIds) },
|
||||
});
|
||||
|
||||
for (const record of records) {
|
||||
record[joinField.name] = relatedRecords.find(
|
||||
(relatedRecord) => relatedRecord.id === record[joinColumnName],
|
||||
);
|
||||
}
|
||||
for (const record of records) {
|
||||
record[joinField.name] = relatedRecords.find(
|
||||
(relatedRecord) => relatedRecord.id === record[joinColumnName],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
private async shouldIgnoreEvent(
|
||||
@@ -339,50 +335,47 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
automatedTriggerTableName,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
automatedTriggerTableName,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const eventListeners = await workflowAutomatedTriggerRepository.find({
|
||||
where: {
|
||||
type: AutomatedTriggerType.DATABASE_EVENT,
|
||||
settings: Raw(
|
||||
() =>
|
||||
`"${automatedTriggerTableName}"."settings"->>'eventName' = :eventName`,
|
||||
{ eventName: databaseEventName },
|
||||
),
|
||||
},
|
||||
});
|
||||
const eventListeners = await workflowAutomatedTriggerRepository.find({
|
||||
where: {
|
||||
type: AutomatedTriggerType.DATABASE_EVENT,
|
||||
settings: Raw(
|
||||
() =>
|
||||
`"${automatedTriggerTableName}"."settings"->>'eventName' = :eventName`,
|
||||
{ eventName: databaseEventName },
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
for (const eventListener of eventListeners) {
|
||||
for (const eventPayload of payload.events) {
|
||||
const shouldTriggerJob = this.shouldTriggerJob({
|
||||
eventPayload,
|
||||
eventListener,
|
||||
action,
|
||||
});
|
||||
for (const eventListener of eventListeners) {
|
||||
for (const eventPayload of payload.events) {
|
||||
const shouldTriggerJob = this.shouldTriggerJob({
|
||||
eventPayload,
|
||||
eventListener,
|
||||
action,
|
||||
});
|
||||
|
||||
if (shouldTriggerJob) {
|
||||
await this.messageQueueService.add<WorkflowTriggerJobData>(
|
||||
WorkflowTriggerJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
workflowId: eventListener.workflowId,
|
||||
payload: eventPayload,
|
||||
},
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
}
|
||||
if (shouldTriggerJob) {
|
||||
await this.messageQueueService.add<WorkflowTriggerJobData>(
|
||||
WorkflowTriggerJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
workflowId: eventListener.workflowId,
|
||||
payload: eventPayload,
|
||||
},
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
private shouldTriggerJob({
|
||||
|
||||
+70
-73
@@ -44,81 +44,78 @@ export class WorkflowTriggerJob {
|
||||
async handle(data: WorkflowTriggerJobData): Promise<void> {
|
||||
const authContext = buildSystemAuthContext(data.workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
try {
|
||||
const workflowRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
|
||||
data.workspaceId,
|
||||
'workflow',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
try {
|
||||
const workflowRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
|
||||
data.workspaceId,
|
||||
'workflow',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflow = await workflowRepository.findOneBy({
|
||||
id: data.workflowId,
|
||||
});
|
||||
const workflow = await workflowRepository.findOneBy({
|
||||
id: data.workflowId,
|
||||
});
|
||||
|
||||
if (!workflow) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow ${data.workflowId} not found in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workflow.lastPublishedVersionId) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow ${data.workflowId} has no published version in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
data.workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOneBy({
|
||||
id: workflow.lastPublishedVersionId,
|
||||
});
|
||||
|
||||
if (!workflowVersion) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow version ${workflow.lastPublishedVersionId} not found in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow version ${workflowVersion.id} is not active in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workflowRunnerWorkspaceService.run({
|
||||
workspaceId: data.workspaceId,
|
||||
workflowVersionId: workflow.lastPublishedVersionId,
|
||||
payload: data.payload,
|
||||
source: {
|
||||
source: FieldActorSource.WORKFLOW,
|
||||
name:
|
||||
isDefined(workflow.name) && !isEmpty(workflow.name)
|
||||
? workflow.name
|
||||
: DEFAULT_WORKFLOW_NAME,
|
||||
context: {},
|
||||
workspaceMemberId: null,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
await this.messageQueueService.removeCron({
|
||||
jobName: WorkflowTriggerJob.name,
|
||||
jobId: data.workflowId,
|
||||
});
|
||||
handleWorkflowTriggerException(e);
|
||||
if (!workflow) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow ${data.workflowId} not found in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!workflow.lastPublishedVersionId) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow ${data.workflowId} has no published version in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
data.workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOneBy({
|
||||
id: workflow.lastPublishedVersionId,
|
||||
});
|
||||
|
||||
if (!workflowVersion) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow version ${workflow.lastPublishedVersionId} not found in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
|
||||
throw new WorkflowTriggerException(
|
||||
`Workflow version ${workflowVersion.id} is not active in workspace ${data.workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workflowRunnerWorkspaceService.run({
|
||||
workspaceId: data.workspaceId,
|
||||
workflowVersionId: workflow.lastPublishedVersionId,
|
||||
payload: data.payload,
|
||||
source: {
|
||||
source: FieldActorSource.WORKFLOW,
|
||||
name:
|
||||
isDefined(workflow.name) && !isEmpty(workflow.name)
|
||||
? workflow.name
|
||||
: DEFAULT_WORKFLOW_NAME,
|
||||
context: {},
|
||||
workspaceMemberId: null,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
await this.messageQueueService.removeCron({
|
||||
jobName: WorkflowTriggerJob.name,
|
||||
jobId: data.workflowId,
|
||||
});
|
||||
handleWorkflowTriggerException(e);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -82,7 +82,6 @@ export class WorkflowTriggerWorkspaceService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
@@ -132,6 +131,7 @@ export class WorkflowTriggerWorkspaceService {
|
||||
|
||||
return true;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ export class WorkflowTriggerWorkspaceService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
@@ -159,6 +158,7 @@ export class WorkflowTriggerWorkspaceService {
|
||||
|
||||
return true;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user