Refactor WorkspaceAuthContext to use discriminated union types (#17491)

## Context
The previous WorkspaceAuthContext was a single interface with many
optional fields, making it unclear which fields are available in
different authentication scenarios. This made the code harder to reason
about and required runtime checks scattered throughout the codebase.

## Changes
- Introduced a discriminated union type for WorkspaceAuthContext with
four specific variants:
-> UserWorkspaceAuthContext - for authenticated users
-> ApiKeyWorkspaceAuthContext - for API key authentication
-> ApplicationWorkspaceAuthContext - for application-based auth
-> SystemWorkspaceAuthContext - for system/internal operations
- Added type guard functions (isUserAuthContext, isApiKeyAuthContext,
etc.) for safe type narrowing
- Added builder utilities (buildUserAuthContext, buildApiKeyAuthContext,
etc.) to construct each context variant with proper type safety
- Refactored WorkspaceAuthContextMiddleware to use the new builders
instead of constructing a loosely-typed object
- Moved the type definition from twenty-orm/interfaces/ to
core-modules/auth/types/ for better organization
- Updated all consumers across query runners, tool providers, and
modules to use the new type location


## Notes
- I had to query User and WorkspaceMember in some parts of tool module
that were expecting userWorkspaceId but not the rest of
UserWorkspaceAuthContext (that should be required with the new proper
type otherwise it would break a lot of logic and mostly permissions with
the newly added RLS -> This is what we expect from
UserWorkspaceAuthContext and how it's done in the "normal" path in HTTP
middleware)
- WorkspaceMember is in the cache already but ideally we should move
User (And Workspace?) in the cache as well to avoid querying the DB
after each request (this is also valid for HTTP middleware when we
hydrate the Request object btw)
This commit is contained in:
Weiko
2026-01-28 18:50:42 +01:00
committed by GitHub
parent 59f7582463
commit fc908e9d87
63 changed files with 544 additions and 186 deletions
@@ -3,7 +3,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { type PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor';
@@ -24,14 +23,17 @@ import {
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonQueryResult } from 'src/engine/api/common/types/common-query-result.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { isWorkspaceAuthContext } from 'src/engine/core-modules/auth/guards/is-workspace-auth-context.guard';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
@@ -287,10 +289,14 @@ export abstract class CommonBaseQueryRunnerService<
const userHasPermission =
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId: authContext.userWorkspaceId,
userWorkspaceId: isUserAuthContext(authContext)
? authContext.userWorkspaceId
: undefined,
setting: permissionRequired,
workspaceId: workspace.id,
apiKeyId: authContext.apiKey?.id,
apiKeyId: isApiKeyAuthContext(authContext)
? authContext.apiKey.id
: undefined,
});
if (!userHasPermission) {
@@ -363,7 +369,7 @@ export abstract class CommonBaseQueryRunnerService<
private async throttleQueryExecution(authContext: WorkspaceAuthContext) {
try {
if (!isDefined(authContext.apiKey)) return;
if (!isApiKeyAuthContext(authContext)) return;
const workspaceId = authContext.workspace.id;
@@ -6,8 +6,7 @@ import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, In, InsertResult, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { PartialObjectRecordWithId } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/types/partial-object-record-with-id.type';
import { buildWhereConditions } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/build-where-conditions.util';
@@ -2,8 +2,7 @@ import { Injectable } from '@nestjs/common';
import { type ObjectRecord } from 'twenty-shared/types';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
@@ -5,8 +5,7 @@ import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -4,8 +4,7 @@ import { msg } from '@lingui/core/macro';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service';
import {
@@ -5,8 +5,7 @@ import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -4,8 +4,7 @@ import { msg } from '@lingui/core/macro';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service';
import {
@@ -9,8 +9,7 @@ import { ObjectRecord, OrderByDirection } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, In, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -8,12 +8,12 @@ import {
import { ObjectRecord, OrderByDirection } from 'twenty-shared/types';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import {
ObjectRecordFilter,
ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -6,9 +6,9 @@ import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -15,9 +15,9 @@ import {
} from 'twenty-shared/utils';
import { ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -15,8 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, In, ObjectLiteral } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -5,8 +5,7 @@ import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -4,8 +4,7 @@ import { msg } from '@lingui/core/macro';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service';
import {
@@ -5,8 +5,7 @@ import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type ObjectRecord } from 'twenty-shared/types';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service';
import {
@@ -1,27 +0,0 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
interface AuthContextWithDefinedWorkspaceProperties {
user: AuthContext['user'];
application: AuthContext['application'];
workspace: NonNullable<AuthContext['workspace']>;
workspaceMetadataVersion?: string;
workspaceMemberId: AuthContext['workspaceMemberId'];
workspaceMember: AuthContext['workspaceMember'];
userWorkspaceId: AuthContext['userWorkspaceId'];
apiKey: AuthContext['apiKey'];
}
interface ApiKeyAuthContext extends Request {
apiKey: NonNullable<AuthContext['apiKey']>;
}
interface UserWorkspaceAuthContext extends Request {
userWorkspaceId: NonNullable<AuthContext['userWorkspaceId']>;
}
interface ApplicationAuthContext extends Request {
application: NonNullable<AuthContext['application']>;
}
export type WorkspaceAuthContext = AuthContextWithDefinedWorkspaceProperties &
(ApiKeyAuthContext | UserWorkspaceAuthContext | ApplicationAuthContext);
@@ -1,7 +1,6 @@
import { type ObjectLiteral } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { type GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { type GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource';
@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { McpCoreController } from 'src/engine/api/mcp/controllers/mcp-core.controller';
import { McpMetadataController } from 'src/engine/api/mcp/controllers/mcp-metadata.controller';
@@ -10,8 +11,11 @@ import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
@@ -22,6 +26,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
MetricsModule,
UserRoleModule,
ToolProviderModule,
TypeOrmModule.forFeature([UserEntity, UserWorkspaceEntity]),
WorkspaceCacheModule,
],
controllers: [McpCoreController, McpMetadataController],
exports: [McpProtocolService],
@@ -1,5 +1,6 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { jsonSchema } from 'ai';
@@ -12,8 +13,10 @@ import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
describe('McpProtocolService', () => {
let service: McpProtocolService;
@@ -22,15 +25,32 @@ describe('McpProtocolService', () => {
let userRoleService: jest.Mocked<UserRoleService>;
let mcpToolExecutorService: jest.Mocked<McpToolExecutorService>;
let apiKeyRoleService: jest.Mocked<ApiKeyRoleService>;
let userWorkspaceRepository: jest.Mocked<{ findOne: jest.Mock }>;
let workspaceCacheService: jest.Mocked<{ getOrRecompute: jest.Mock }>;
const mockWorkspace = { id: 'workspace-1' } as WorkspaceEntity;
const mockUserWorkspaceId = 'user-workspace-1';
const mockUserId = 'user-1';
const mockWorkspaceMemberId = 'workspace-member-1';
const mockRoleId = 'role-1';
const mockAdminRoleId = 'admin-role-1';
const mockApiKey = {
id: 'api-key-1',
workspaceId: mockWorkspace.id,
} as ApiKeyEntity;
const mockUser = { id: mockUserId };
const mockUserWorkspace = {
id: mockUserWorkspaceId,
user: mockUser,
};
const mockWorkspaceMember = {
id: mockWorkspaceMemberId,
userId: mockUserId,
};
const mockFlatWorkspaceMemberMaps = {
idByUserId: { [mockUserId]: mockWorkspaceMemberId },
byId: { [mockWorkspaceMemberId]: mockWorkspaceMember },
};
beforeEach(async () => {
const mockFeatureFlagService = {
@@ -54,6 +74,15 @@ describe('McpProtocolService', () => {
getRoleIdForApiKeyId: jest.fn().mockResolvedValue(mockAdminRoleId),
};
const mockWorkspaceCacheService = {
getOrRecompute: jest.fn(),
invalidateAndRecompute: jest.fn(),
};
const mockUserWorkspaceRepository = {
findOne: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
McpProtocolService,
@@ -77,6 +106,14 @@ describe('McpProtocolService', () => {
provide: ApiKeyRoleService,
useValue: mockApiKeyRoleService,
},
{
provide: WorkspaceCacheService,
useValue: mockWorkspaceCacheService,
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: mockUserWorkspaceRepository,
},
],
}).compile();
@@ -86,6 +123,10 @@ describe('McpProtocolService', () => {
userRoleService = module.get(UserRoleService);
mcpToolExecutorService = module.get(McpToolExecutorService);
apiKeyRoleService = module.get(ApiKeyRoleService);
userWorkspaceRepository = module.get(
getRepositoryToken(UserWorkspaceEntity),
);
workspaceCacheService = module.get(WorkspaceCacheService);
});
it('should be defined', () => {
@@ -216,6 +257,10 @@ describe('McpProtocolService', () => {
it('should handle tools/call method with userWorkspaceId', async () => {
featureFlagService.isFeatureEnabled.mockResolvedValue(true);
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
userWorkspaceRepository.findOne.mockResolvedValue(mockUserWorkspace);
workspaceCacheService.getOrRecompute.mockResolvedValue({
flatWorkspaceMemberMaps: mockFlatWorkspaceMemberMaps,
});
const mockTool = {
description: 'Test tool',
@@ -322,6 +367,10 @@ describe('McpProtocolService', () => {
it('should handle tools listing', async () => {
featureFlagService.isFeatureEnabled.mockResolvedValue(true);
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
userWorkspaceRepository.findOne.mockResolvedValue(mockUserWorkspace);
workspaceCacheService.getOrRecompute.mockResolvedValue({
flatWorkspaceMemberMaps: mockFlatWorkspaceMemberMaps,
});
const mockToolsMap = {
testTool: {
@@ -398,6 +447,10 @@ describe('McpProtocolService', () => {
it('should handle error when tool is not found', async () => {
featureFlagService.isFeatureEnabled.mockResolvedValue(true);
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
userWorkspaceRepository.findOne.mockResolvedValue(mockUserWorkspace);
workspaceCacheService.getOrRecompute.mockResolvedValue({
flatWorkspaceMemberMaps: mockFlatWorkspaceMemberMaps,
});
toolRegistryService.getToolsByCategories.mockResolvedValue({});
mcpToolExecutorService.handleToolCall.mockRejectedValue(
@@ -1,21 +1,27 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { Repository } from 'typeorm';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildApiKeyAuthContext } from 'src/engine/core-modules/auth/utils/build-api-key-auth-context.util';
import { buildSystemAuthContext } from 'src/engine/core-modules/auth/utils/build-system-auth-context.util';
import { buildUserAuthContext } from 'src/engine/core-modules/auth/utils/build-user-auth-context.util';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@Injectable()
export class McpProtocolService {
@@ -25,6 +31,9 @@ export class McpProtocolService {
private readonly userRoleService: UserRoleService,
private readonly mcpToolExecutorService: McpToolExecutorService,
private readonly apiKeyRoleService: ApiKeyRoleService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
async checkAiEnabled(workspaceId: string): Promise<void> {
@@ -87,19 +96,59 @@ export class McpProtocolService {
return roleId;
}
private buildAuthContext(
private async buildAuthContext(
workspace: WorkspaceEntity,
userWorkspaceId?: string,
apiKey?: ApiKeyEntity,
): WorkspaceAuthContext {
return {
user: null,
apiKey: apiKey ?? null,
application: null,
workspace,
workspaceMemberId: undefined,
userWorkspaceId: userWorkspaceId ?? undefined,
} as WorkspaceAuthContext;
): Promise<WorkspaceAuthContext> {
if (isDefined(apiKey)) {
return buildApiKeyAuthContext({ workspace, apiKey });
}
if (isDefined(userWorkspaceId)) {
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: {
id: userWorkspaceId,
},
relations: {
user: true,
},
});
const user = userWorkspace?.user;
if (!isDefined(user)) {
throw new HttpException('User not found', HttpStatus.FORBIDDEN);
}
const { flatWorkspaceMemberMaps } =
await this.workspaceCacheService.getOrRecompute(workspace.id, [
'flatWorkspaceMemberMaps',
]);
const workspaceMemberId = flatWorkspaceMemberMaps.idByUserId[user.id];
const workspaceMember = isDefined(workspaceMemberId)
? flatWorkspaceMemberMaps.byId[workspaceMemberId]
: undefined;
if (!isDefined(workspaceMemberId) || !isDefined(workspaceMember)) {
throw new HttpException(
'Workspace member not found',
HttpStatus.FORBIDDEN,
);
}
return buildUserAuthContext({
workspace,
userWorkspaceId,
user,
workspaceMemberId,
workspaceMember,
});
}
return buildSystemAuthContext({ workspace });
}
async handleMCPCoreQuery(
@@ -137,7 +186,7 @@ export class McpProtocolService {
apiKey,
);
const authContext = this.buildAuthContext(
const authContext = await this.buildAuthContext(
workspace,
userWorkspaceId,
apiKey,
@@ -7,8 +7,6 @@ import {
isDefined,
} from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
import { CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
@@ -17,7 +15,11 @@ import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.typ
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { ActorFromAuthContextService } from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { getWorkspaceAuthContext } from 'src/engine/core-modules/auth/storage/workspace-auth-context.storage';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@@ -79,28 +81,15 @@ export abstract class RestApiBaseHandler {
FormatResult | { data: FormatResult[] } | CommonGroupByOutputItem[]
>;
public getAuthContextFromRequest(
request: AuthenticatedRequest,
): WorkspaceAuthContext {
return request;
}
private getObjectsPermissions = async (authContext: WorkspaceAuthContext) => {
let roleId: string;
if (isDefined(authContext.apiKey)) {
if (isApiKeyAuthContext(authContext)) {
roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId(
authContext.apiKey.id,
authContext.workspace.id,
);
} else {
if (!isDefined(authContext.userWorkspaceId)) {
throw new PermissionsException(
'No user workspace ID found in authentication context',
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
);
}
} else if (isUserAuthContext(authContext)) {
const userWorkspaceRoleId =
await this.userRoleService.getRoleIdForUserWorkspace({
userWorkspaceId: authContext.userWorkspaceId,
@@ -115,6 +104,11 @@ export abstract class RestApiBaseHandler {
}
roleId = userWorkspaceRoleId;
} else {
throw new PermissionsException(
'Authentication context is invalid',
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
);
}
const { rolesPermissions } =
@@ -161,7 +155,7 @@ export abstract class RestApiBaseHandler {
objectIdByNameSingular,
} = await this.getObjectMetadata(request, parsedObject);
const authContext = this.getAuthContextFromRequest(request);
const authContext = getWorkspaceAuthContext();
return {
authContext,
@@ -1,5 +1,5 @@
import { type Request } from 'express';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export type AuthenticatedRequest = Request & WorkspaceAuthContext;
@@ -3,8 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { type ActorMetadata } from 'twenty-shared/types';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util';
import { buildCreatedByFromApplication } from 'src/engine/core-modules/actor/utils/build-created-by-from-application.util';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
@@ -0,0 +1,10 @@
import {
type ApiKeyWorkspaceAuthContext,
type WorkspaceAuthContext,
} from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export const isApiKeyAuthContext = (
context: WorkspaceAuthContext,
): context is ApiKeyWorkspaceAuthContext => {
return context.type === 'apiKey';
};
@@ -0,0 +1,10 @@
import {
type ApplicationWorkspaceAuthContext,
type WorkspaceAuthContext,
} from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export const isApplicationAuthContext = (
context: WorkspaceAuthContext,
): context is ApplicationWorkspaceAuthContext => {
return context.type === 'application';
};
@@ -0,0 +1,10 @@
import {
type PendingActivationUserWorkspaceAuthContext,
type WorkspaceAuthContext,
} from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export const isPendingActivationUserAuthContext = (
context: WorkspaceAuthContext,
): context is PendingActivationUserWorkspaceAuthContext => {
return context.type === 'pendingActivationUser';
};
@@ -0,0 +1,10 @@
import {
type SystemWorkspaceAuthContext,
type WorkspaceAuthContext,
} from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export const isSystemAuthContext = (
context: WorkspaceAuthContext,
): context is SystemWorkspaceAuthContext => {
return context.type === 'system';
};
@@ -0,0 +1,10 @@
import {
type UserWorkspaceAuthContext,
type WorkspaceAuthContext,
} from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export const isUserAuthContext = (
context: WorkspaceAuthContext,
): context is UserWorkspaceAuthContext => {
return context.type === 'user';
};
@@ -1,8 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export const isWorkspaceAuthContext = (
context: AuthContext,
@@ -3,9 +3,16 @@ import { Injectable, type NestMiddleware } from '@nestjs/common';
import { type NextFunction, type Request, type Response } from 'express';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { withWorkspaceAuthContext } from 'src/engine/core-modules/auth/storage/workspace-auth-context.storage';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildApiKeyAuthContext } from 'src/engine/core-modules/auth/utils/build-api-key-auth-context.util';
import { buildApplicationAuthContext } from 'src/engine/core-modules/auth/utils/build-application-auth-context.util';
import { buildPendingActivationUserAuthContext } from 'src/engine/core-modules/auth/utils/build-pending-activation-user-auth-context.util';
import { buildUserAuthContext } from 'src/engine/core-modules/auth/utils/build-user-auth-context.util';
@Injectable()
export class WorkspaceAuthContextMiddleware implements NestMiddleware {
@@ -16,18 +23,54 @@ export class WorkspaceAuthContextMiddleware implements NestMiddleware {
return;
}
const authContext: WorkspaceAuthContext = {
user: req.user,
workspace: req.workspace,
workspaceMemberId: req.workspaceMemberId,
workspaceMember: req.workspaceMember,
userWorkspaceId: req.userWorkspaceId,
apiKey: req.apiKey,
application: req.application,
} as WorkspaceAuthContext;
const authContext = this.buildAuthContext(req);
withWorkspaceAuthContext(authContext, () => {
next();
});
}
private buildAuthContext(req: Request): WorkspaceAuthContext {
if (isDefined(req.apiKey)) {
return buildApiKeyAuthContext({
workspace: req.workspace!,
apiKey: req.apiKey,
});
}
if (isDefined(req.application)) {
return buildApplicationAuthContext({
workspace: req.workspace!,
application: req.application,
});
}
if (
isDefined(req.userWorkspaceId) &&
isDefined(req.workspaceMemberId) &&
isDefined(req.workspaceMember) &&
isDefined(req.user)
) {
return buildUserAuthContext({
workspace: req.workspace!,
userWorkspaceId: req.userWorkspaceId,
user: req.user,
workspaceMemberId: req.workspaceMemberId,
workspaceMember: req.workspaceMember,
});
}
if (isDefined(req.userWorkspaceId) && isDefined(req.user)) {
return buildPendingActivationUserAuthContext({
workspace: req.workspace!,
userWorkspaceId: req.userWorkspaceId,
user: req.user,
});
}
throw new AuthException(
'No authentication context found',
AuthExceptionCode.UNAUTHENTICATED,
);
}
}
@@ -1,6 +1,6 @@
import { AsyncLocalStorage } from 'async_hooks';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
export const workspaceAuthContextStorage =
new AsyncLocalStorage<WorkspaceAuthContext>();
@@ -0,0 +1,51 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
export type WorkspaceAuthContextType =
| 'system'
| 'user'
| 'apiKey'
| 'application'
| 'pendingActivationUser';
interface BaseWorkspaceAuthContext {
type: WorkspaceAuthContextType;
workspace: NonNullable<AuthContext['workspace']>;
workspaceMetadataVersion?: string;
}
export interface ApiKeyWorkspaceAuthContext extends BaseWorkspaceAuthContext {
type: 'apiKey';
apiKey: NonNullable<AuthContext['apiKey']>;
}
export interface UserWorkspaceAuthContext extends BaseWorkspaceAuthContext {
type: 'user';
userWorkspaceId: NonNullable<AuthContext['userWorkspaceId']>;
user: NonNullable<AuthContext['user']>;
workspaceMemberId: NonNullable<AuthContext['workspaceMemberId']>;
workspaceMember: NonNullable<AuthContext['workspaceMember']>;
}
export interface ApplicationWorkspaceAuthContext
extends BaseWorkspaceAuthContext {
type: 'application';
application: NonNullable<AuthContext['application']>;
}
export interface SystemWorkspaceAuthContext extends BaseWorkspaceAuthContext {
type: 'system';
}
export interface PendingActivationUserWorkspaceAuthContext
extends BaseWorkspaceAuthContext {
type: 'pendingActivationUser';
userWorkspaceId: NonNullable<AuthContext['userWorkspaceId']>;
user: NonNullable<AuthContext['user']>;
}
export type WorkspaceAuthContext =
| ApiKeyWorkspaceAuthContext
| UserWorkspaceAuthContext
| ApplicationWorkspaceAuthContext
| SystemWorkspaceAuthContext
| PendingActivationUserWorkspaceAuthContext;
@@ -0,0 +1,19 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type ApiKeyWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
type ApiKeyAuthContextInput = {
workspace: NonNullable<AuthContext['workspace']>;
apiKey: NonNullable<AuthContext['apiKey']>;
workspaceMetadataVersion?: string;
};
export const buildApiKeyAuthContext = (
input: ApiKeyAuthContextInput,
): ApiKeyWorkspaceAuthContext => {
return {
type: 'apiKey',
workspace: input.workspace,
apiKey: input.apiKey,
workspaceMetadataVersion: input.workspaceMetadataVersion,
};
};
@@ -0,0 +1,19 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type ApplicationWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
type ApplicationAuthContextInput = {
workspace: NonNullable<AuthContext['workspace']>;
application: NonNullable<AuthContext['application']>;
workspaceMetadataVersion?: string;
};
export const buildApplicationAuthContext = (
input: ApplicationAuthContextInput,
): ApplicationWorkspaceAuthContext => {
return {
type: 'application',
workspace: input.workspace,
application: input.application,
workspaceMetadataVersion: input.workspaceMetadataVersion,
};
};
@@ -0,0 +1,21 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type PendingActivationUserWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
type PendingActivationUserAuthContextInput = {
workspace: NonNullable<AuthContext['workspace']>;
userWorkspaceId: NonNullable<AuthContext['userWorkspaceId']>;
user: NonNullable<AuthContext['user']>;
workspaceMetadataVersion?: string;
};
export const buildPendingActivationUserAuthContext = (
input: PendingActivationUserAuthContextInput,
): PendingActivationUserWorkspaceAuthContext => {
return {
type: 'pendingActivationUser',
workspace: input.workspace,
userWorkspaceId: input.userWorkspaceId,
user: input.user,
workspaceMetadataVersion: input.workspaceMetadataVersion,
};
};
@@ -0,0 +1,17 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type SystemWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
type SystemAuthContextInput = {
workspace: NonNullable<AuthContext['workspace']>;
workspaceMetadataVersion?: string;
};
export const buildSystemAuthContext = (
input: SystemAuthContextInput,
): SystemWorkspaceAuthContext => {
return {
type: 'system',
workspace: input.workspace,
workspaceMetadataVersion: input.workspaceMetadataVersion,
};
};
@@ -0,0 +1,25 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type UserWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
type UserAuthContextInput = {
workspace: NonNullable<AuthContext['workspace']>;
userWorkspaceId: NonNullable<AuthContext['userWorkspaceId']>;
user: NonNullable<AuthContext['user']>;
workspaceMemberId: NonNullable<AuthContext['workspaceMemberId']>;
workspaceMember: NonNullable<AuthContext['workspaceMember']>;
workspaceMetadataVersion?: string;
};
export const buildUserAuthContext = (
input: UserAuthContextInput,
): UserWorkspaceAuthContext => {
return {
type: 'user',
workspace: input.workspace,
userWorkspaceId: input.userWorkspaceId,
user: input.user,
workspaceMemberId: input.workspaceMemberId,
workspaceMember: input.workspaceMember,
workspaceMetadataVersion: input.workspaceMetadataVersion,
};
};
@@ -3,12 +3,14 @@ import { Injectable } from '@nestjs/common';
import { type ObjectsPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import {
RecordCrudException,
RecordCrudExceptionCode,
@@ -117,14 +119,17 @@ export class CommonApiContextBuilderService {
const workspaceId = authContext.workspace.id;
let roleId: string;
if (isDefined(authContext.apiKey)) {
if (isApiKeyAuthContext(authContext)) {
roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId(
authContext.apiKey.id,
workspaceId,
);
} else if (isDefined(authContext.application?.defaultLogicFunctionRoleId)) {
} else if (
isApplicationAuthContext(authContext) &&
isDefined(authContext.application.defaultLogicFunctionRoleId)
) {
roleId = authContext.application.defaultLogicFunctionRoleId;
} else if (isDefined(authContext.userWorkspaceId)) {
} else if (isUserAuthContext(authContext)) {
const userWorkspaceRoleId =
await this.userRoleService.getRoleIdForUserWorkspace({
userWorkspaceId: authContext.userWorkspaceId,
@@ -1,5 +1,4 @@
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
export type RecordCrudExecutionContext = {
@@ -4,8 +4,7 @@ import {
type RestrictedFieldsPermissions,
} from 'twenty-shared/types';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
@@ -2,8 +2,7 @@ import { type ToolSet } from 'ai';
import { type CodeExecutionData } from 'twenty-shared/ai';
import { type ActorMetadata } from 'twenty-shared/types';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { type ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type ToolSet } from 'ai';
import {
@@ -6,20 +7,28 @@ import {
type ObjectsPermissionsByRoleId,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import {
type ToolProvider,
type ToolProviderContext,
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildUserAuthContext } from 'src/engine/core-modules/auth/utils/build-user-auth-context.util';
import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service';
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
import { createDirectRecordToolsFactory } from 'src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
@@ -36,6 +45,8 @@ export class DatabaseToolProvider implements ToolProvider {
private readonly updateRecordService: UpdateRecordService,
private readonly deleteRecordService: DeleteRecordService,
private readonly findRecordsService: FindRecordsService,
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
) {}
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
@@ -46,22 +57,49 @@ export class DatabaseToolProvider implements ToolProvider {
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
const tools: ToolSet = {};
// Build authContext from available context info
// userWorkspaceId is required for user-based tool generation
if (!context.userWorkspaceId) {
// Both userId and userWorkspaceId are required for user-based tool generation
if (!isDefined(context.userId) || !isDefined(context.userWorkspaceId)) {
return tools;
}
const authContext: WorkspaceAuthContext = {
user: context.userId ? { id: context.userId } : null,
workspace: {
id: context.workspaceId,
} as WorkspaceAuthContext['workspace'],
workspaceMemberId: undefined,
const user = await this.userRepository.findOne({
where: {
id: context.userId,
},
});
if (!isDefined(user)) {
throw new AuthException(
'User not found',
AuthExceptionCode.UNAUTHENTICATED,
);
}
const { flatWorkspaceMemberMaps } =
await this.workspaceCacheService.getOrRecompute(context.workspaceId, [
'flatWorkspaceMemberMaps',
]);
const workspaceMemberId = flatWorkspaceMemberMaps.idByUserId[user.id];
const workspaceMember = isDefined(workspaceMemberId)
? flatWorkspaceMemberMaps.byId[workspaceMemberId]
: undefined;
if (!isDefined(workspaceMemberId) || !isDefined(workspaceMember)) {
throw new AuthException(
'Workspace member not found',
AuthExceptionCode.UNAUTHENTICATED,
);
}
const authContext: WorkspaceAuthContext = buildUserAuthContext({
workspace: { id: context.workspaceId } as WorkspaceEntity,
userWorkspaceId: context.userWorkspaceId,
apiKey: null,
application: null,
} as WorkspaceAuthContext;
user,
workspaceMemberId,
workspaceMember,
});
const { rolesPermissions } =
await this.workspaceCacheService.getOrRecompute(context.workspaceId, [
@@ -4,8 +4,10 @@ import { Field, ObjectType, Query, Resolver } from '@nestjs/graphql';
import graphqlTypeJson from 'graphql-type-json';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -40,6 +42,7 @@ export class ToolIndexResolver {
@Query(() => [ToolIndexEntryDTO])
@UseGuards(NoPermissionGuard)
async getToolIndex(
@AuthUser({ allowUndefined: true }) user: UserEntity | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string,
): Promise<ToolIndexEntryDTO[]> {
@@ -53,6 +56,7 @@ export class ToolIndexResolver {
}
return this.toolRegistryService.buildToolIndex(workspace.id, roleId, {
userId: user?.id,
userWorkspaceId,
});
}
@@ -1,4 +1,5 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-crud.module';
import { ToolGeneratorModule } from 'src/engine/core-modules/tool-generator/tool-generator.module';
@@ -12,6 +13,7 @@ import { LogicFunctionToolProvider } from 'src/engine/core-modules/tool-provider
import { ViewToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-tool.provider';
import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider';
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
@@ -46,6 +48,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
WorkspaceManyOrAllFlatEntityMapsCacheModule,
LogicFunctionModule,
UserRoleModule,
TypeOrmModule.forFeature([UserEntity]),
],
providers: [
ToolIndexResolver,
@@ -9,11 +9,13 @@ import {
type ToolSet,
} from 'ai';
import { type ActorMetadata } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type Repository } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type';
@@ -130,8 +132,14 @@ export class AgentAsyncExecutorService {
authContext,
actorContext,
agent: agent as unknown as ToolProviderContext['agent'],
userId: authContext?.user?.id,
userWorkspaceId: authContext?.userWorkspaceId,
userId:
isDefined(authContext) && isUserAuthContext(authContext)
? authContext.user.id
: undefined,
userWorkspaceId:
isDefined(authContext) && isUserAuthContext(authContext)
? authContext.userWorkspaceId
: undefined,
},
{
categories: [
@@ -11,8 +11,7 @@ import {
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -2,10 +2,12 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { getRecordDisplayName } from 'src/engine/core-modules/record-crud/utils/get-record-display-name.util';
import { getRecordImageIdentifier } from 'src/engine/core-modules/record-crud/utils/get-record-image-identifier.util';
@@ -356,18 +358,21 @@ export class NavigationMenuItemService {
authContext: WorkspaceAuthContext,
workspaceId: string,
): Promise<string | undefined> {
if (isDefined(authContext.apiKey)) {
if (isApiKeyAuthContext(authContext)) {
return this.apiKeyRoleService.getRoleIdForApiKeyId(
authContext.apiKey.id,
workspaceId,
);
}
if (isDefined(authContext.application?.defaultLogicFunctionRoleId)) {
if (
isApplicationAuthContext(authContext) &&
isDefined(authContext.application.defaultLogicFunctionRoleId)
) {
return authContext.application.defaultLogicFunctionRoleId;
}
if (isDefined(authContext.userWorkspaceId)) {
if (isUserAuthContext(authContext)) {
try {
return await this.userRoleService.getRoleIdForUserWorkspace({
userWorkspaceId: authContext.userWorkspaceId,
@@ -6,9 +6,9 @@ import { EntityManager } from 'typeorm';
import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor';
import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -33,10 +33,10 @@ import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/tr
import { type UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { InstanceChecker } from 'typeorm/util/InstanceChecker';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -13,9 +13,9 @@ import {
import { EntityManagerFactory } from 'typeorm/entity-manager/EntityManagerFactory';
import { EntityMetadataNotFoundError } from 'typeorm/error/EntityMetadataNotFoundError';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import {
PermissionsException,
PermissionsExceptionCode,
@@ -2,8 +2,7 @@ import { Injectable, type Type } from '@nestjs/common';
import { type ObjectLiteral } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { getWorkspaceAuthContext } from 'src/engine/core-modules/auth/storage/workspace-auth-context.storage';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource';
@@ -3,8 +3,7 @@ import { AsyncLocalStorage } from 'async_hooks';
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
import { type EntityMetadata } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -1,15 +1,13 @@
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type SystemWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildSystemAuthContext as buildSystemAuthContextUtil } from 'src/engine/core-modules/auth/utils/build-system-auth-context.util';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
// Builds a minimal WorkspaceAuthContext for system operations (jobs, commands, crons)
// that don't have a user context but need to operate on a workspace
export const buildSystemAuthContext = (
workspaceId: string,
): WorkspaceAuthContext => {
return {
user: null,
workspace: { id: workspaceId } as WorkspaceAuthContext['workspace'],
workspaceMemberId: undefined,
userWorkspaceId: undefined,
apiKey: null,
} as WorkspaceAuthContext;
): SystemWorkspaceAuthContext => {
return buildSystemAuthContextUtil({
workspace: { id: workspaceId } as WorkspaceEntity,
});
};
@@ -3,8 +3,8 @@ import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -6,8 +6,7 @@ import {
isDefined,
} from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { ActorFromAuthContextService } from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -1,8 +1,8 @@
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -1,8 +1,8 @@
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -3,8 +3,9 @@ import { Injectable } from '@nestjs/common';
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildApplicationAuthContext } from 'src/engine/core-modules/auth/utils/build-application-auth-context.util';
import { buildUserAuthContext } from 'src/engine/core-modules/auth/utils/build-user-auth-context.util';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { ADMIN_ROLE } from 'src/engine/metadata-modules/role/constants/admin-role';
@@ -70,14 +71,13 @@ export class WorkflowExecutionContextService {
workspaceId,
});
const authContext = {
user: userWorkspace.user,
apiKey: null,
application: null,
const authContext: WorkspaceAuthContext = buildUserAuthContext({
workspace: userWorkspace.workspace,
workspaceMemberId: workspaceMember.id,
userWorkspaceId: userWorkspace.id,
} as WorkspaceAuthContext;
user: userWorkspace.user,
workspaceMemberId: workspaceMember.id,
workspaceMember,
});
return {
isActingOnBehalfOfUser: true,
@@ -114,17 +114,13 @@ export class WorkflowExecutionContextService {
? { unionOf: [roleId] }
: { shouldBypassPermissionChecks: true as const };
const authContext = {
user: null,
apiKey: null,
const authContext: WorkspaceAuthContext = buildApplicationAuthContext({
workspace,
application: {
...application,
defaultLogicFunctionRoleId: roleId,
},
workspace,
workspaceMemberId: undefined,
userWorkspaceId: undefined,
} as WorkspaceAuthContext;
});
return {
isActingOnBehalfOfUser: false,
@@ -1,7 +1,6 @@
import { type ActorMetadata } from 'twenty-shared/types';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
export type WorkflowExecutionContext = {
@@ -4,8 +4,8 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';