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:
+54
-60
@@ -53,38 +53,35 @@ export class ChannelSyncService {
|
||||
): Promise<void> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId,
|
||||
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId,
|
||||
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
});
|
||||
|
||||
for (const messageChannel of messageChannels) {
|
||||
await this.messageChannelSyncStatusService.markAsMessagesListFetchScheduled(
|
||||
[messageChannel.id],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
||||
MessagingMessageListFetchJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (const messageChannel of messageChannels) {
|
||||
await this.messageChannelSyncStatusService.markAsMessagesListFetchScheduled(
|
||||
[messageChannel.id],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
||||
MessagingMessageListFetchJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
private async startCalendarChannelSync(
|
||||
@@ -93,38 +90,35 @@ export class ChannelSyncService {
|
||||
): Promise<void> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId,
|
||||
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId,
|
||||
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
});
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
await calendarChannelRepository.update(calendarChannel.id, {
|
||||
syncStage:
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
});
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
await calendarChannelRepository.update(calendarChannel.id, {
|
||||
syncStage:
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
});
|
||||
|
||||
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
|
||||
CalendarEventListFetchJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
calendarChannelId: calendarChannel.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
|
||||
CalendarEventListFetchJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
calendarChannelId: calendarChannel.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ describe('Email Alias Manager Service', () => {
|
||||
.mockResolvedValue(connectedAccountRepository),
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
.mockImplementation((_authContext: any, fn: () => any) => fn()),
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
},
|
||||
},
|
||||
EmailAliasManagerService,
|
||||
|
||||
+13
-16
@@ -49,22 +49,19 @@ export class EmailAliasManagerService {
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id },
|
||||
{
|
||||
handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array
|
||||
},
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id },
|
||||
{
|
||||
handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-13
@@ -24,19 +24,16 @@ export class DeleteWorkspaceMemberConnectedAccountsCleanupJob {
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.delete({
|
||||
accountOwnerId: workspaceMemberId,
|
||||
});
|
||||
},
|
||||
);
|
||||
await connectedAccountRepository.delete({
|
||||
accountOwnerId: workspaceMemberId,
|
||||
});
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-28
@@ -27,36 +27,30 @@ export class ConnectedAccountListener {
|
||||
const workspaceId = payload.workspaceId;
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
for (const eventPayload of payload.events) {
|
||||
const workspaceMemberId =
|
||||
eventPayload.properties.before.accountOwnerId;
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
for (const eventPayload of payload.events) {
|
||||
const workspaceMemberId = eventPayload.properties.before.accountOwnerId;
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
|
||||
{
|
||||
where: { id: workspaceMemberId },
|
||||
},
|
||||
);
|
||||
|
||||
const userId = workspaceMember.userId;
|
||||
|
||||
const connectedAccountId = eventPayload.properties.before.id;
|
||||
|
||||
await this.accountsToReconnectService.removeAccountToReconnect(
|
||||
userId,
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
connectedAccountId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
|
||||
const userId = workspaceMember.userId;
|
||||
|
||||
const connectedAccountId = eventPayload.properties.before.id;
|
||||
|
||||
await this.accountsToReconnectService.removeAccountToReconnect(
|
||||
userId,
|
||||
workspaceId,
|
||||
connectedAccountId,
|
||||
);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -41,7 +41,6 @@ export class ConnectedAccountDeleteOnePreQueryHook
|
||||
|
||||
const messageChannels =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext as WorkspaceAuthContext,
|
||||
async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
@@ -53,6 +52,7 @@ export class ConnectedAccountDeleteOnePreQueryHook
|
||||
connectedAccountId,
|
||||
});
|
||||
},
|
||||
authContext as WorkspaceAuthContext,
|
||||
);
|
||||
|
||||
const objectMetadataEntity =
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
|
||||
.mockImplementation((_authContext: any, fn: () => any) => fn()),
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+14
-17
@@ -77,24 +77,21 @@ export class ConnectedAccountRefreshTokensService {
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id },
|
||||
{
|
||||
...connectedAccountTokens,
|
||||
lastCredentialsRefreshedAt: new Date(),
|
||||
},
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id },
|
||||
{
|
||||
...connectedAccountTokens,
|
||||
lastCredentialsRefreshedAt: new Date(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
return connectedAccountTokens;
|
||||
}
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
|
||||
.mockImplementation((_authContext: any, fn: () => any) => fn()),
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+2
-2
@@ -29,7 +29,6 @@ export class ImapSmtpCalDavAPIService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
@@ -41,6 +40,7 @@ export class ImapSmtpCalDavAPIService {
|
||||
where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV },
|
||||
});
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ export class ImapSmtpCalDavAPIService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
@@ -147,6 +146,7 @@ export class ImapSmtpCalDavAPIService {
|
||||
|
||||
return newOrExistingAccountId;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user