From 6e5e7963b5859edbde81bd501dbb71cae0a65876 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 20 May 2026 15:02:58 +0200 Subject: [PATCH] fix(server): map PermissionsException to proper HTTP status on REST API (#20739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `PermissionsException` thrown by `SettingsPermissionGuard` (and other permission code paths) was bubbling up through every typed REST exception filter and landing in the global `UnhandledExceptionFilter`, which falls back to **500** for anything that isn't an `HttpException`. So a forbidden user (e.g. an API key whose role doesn't have `DATA_MODEL`) calling `GET /rest/metadata/objects` got: ``` HTTP/1.1 500 Internal Server Error "Entity performing the request does not have permission" ``` GraphQL already had the right plumbing via `permissionGraphqlApiExceptionHandler` (`ForbiddenError` → 403, `UserInputError` → 400, `NotFoundError` → 404). This PR mirrors it on the REST side. ## What - New util `permissionRestApiExceptionCodeToHttpStatus` mapping every `PermissionsExceptionCode` → HTTP status, with `assertUnreachable` to force explicit handling of future codes. - New filter `PermissionsRestApiExceptionFilter` (`@Catch(PermissionsException)`) that delegates to `HttpExceptionHandlerService.handleError(...)` with the resolved status. - Wired `PermissionsRestApiExceptionFilter` (placed first, so the typed filter wins over any sibling catch-all) into `@UseFilters(...)` of every REST controller that uses `SettingsPermissionGuard` or whose service can throw `PermissionsException`: - `object-metadata`, `field-metadata`, `webhook`, `api-key` - `view`, `view-sort`, `view-group`, `view-filter`, `view-filter-group`, `view-field` - `page-layout`, `page-layout-widget`, `page-layout-tab` - `front-component`, `ai-generate-text` - Unit tests covering 403 / 400 / 404 / 500 mappings. ## Mapping | Code | Status | |------|--------| | `PERMISSION_DENIED`, `NO_AUTHENTICATION_CONTEXT`, `ROLE_LABEL_ALREADY_EXISTS`, `CANNOT_UNASSIGN_LAST_ADMIN`, `CANNOT_UPDATE_SELF_ROLE`, `CANNOT_DELETE_LAST_ADMIN_USER`, `ROLE_NOT_EDITABLE`, `CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT`, `CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT` | **403** | | `INVALID_ARG`, `INVALID_SETTING`, `CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT`, `CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION`, `ONLY_FIELD_RESTRICTION_ALLOWED`, `FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT`, `FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT`, `EMPTY_FIELD_PERMISSION_NOT_ALLOWED`, `ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET`, `ROLE_CANNOT_BE_ASSIGNED_TO_USERS` | **400** | | `ROLE_NOT_FOUND`, `OBJECT_METADATA_NOT_FOUND`, `FIELD_METADATA_NOT_FOUND`, `FIELD_PERMISSION_NOT_FOUND`, `PERMISSION_NOT_FOUND` | **404** | | All remaining "internal" codes (rethrown as-is in GraphQL) | **500** | ## Before Screenshot 2026-05-19 at 19 26 07 ## After Screenshot 2026-05-19 at 19 26 01 --- .../api-key/controllers/api-key.controller.ts | 3 +- .../ai-generate-text.controller.ts | 7 +- .../controllers/field-metadata.controller.ts | 6 +- .../controllers/front-component.controller.ts | 2 + .../controllers/object-metadata.controller.ts | 6 +- .../controllers/page-layout-tab.controller.ts | 2 + .../page-layout-widget.controller.ts | 2 + .../controllers/page-layout.controller.ts | 2 + ...exception-code-to-http-status.util.spec.ts | 76 +++++++++++++++++++ ...-api-exception-code-to-http-status.util.ts | 61 +++++++++++++++ .../permissions-rest-api-exception.filter.ts | 31 ++++++++ .../controllers/view-field.controller.ts | 2 + .../view-filter-group.controller.ts | 2 + .../controllers/view-filter.controller.ts | 2 + .../controllers/view-group.controller.ts | 2 + .../controllers/view-sort.controller.ts | 2 + .../view/controllers/view.controller.ts | 2 + .../webhook/controllers/webhook.controller.ts | 2 + 18 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/permissions/utils/__tests__/permission-rest-api-exception-code-to-http-status.util.spec.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-rest-api-exception-code-to-http-status.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter.ts diff --git a/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts index cd0abf4f7d..676a276ab8 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts @@ -23,6 +23,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; /** * rest/apiKeys is deprecated, use rest/metadata/apiKeys instead @@ -34,7 +35,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS), ) -@UseFilters(RestApiExceptionFilter) +@UseFilters(PermissionsRestApiExceptionFilter, RestApiExceptionFilter) export class ApiKeyController { constructor(private readonly apiKeyService: ApiKeyService) {} diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts index 7d7eb3c029..d6eaed1ad2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts @@ -20,10 +20,15 @@ import { AiBillingService } from 'src/engine/metadata-modules/ai/ai-billing/serv import { AiRestApiExceptionFilter } from 'src/engine/metadata-modules/ai/filters/ai-api-exception.filter'; import { GenerateTextInput } from 'src/engine/metadata-modules/ai/ai-generate-text/dtos/generate-text.input'; import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; @Controller('rest/ai') @UseGuards(JwtAuthGuard, WorkspaceAuthGuard) -@UseFilters(AiRestApiExceptionFilter, RestApiExceptionFilter) +@UseFilters( + PermissionsRestApiExceptionFilter, + AiRestApiExceptionFilter, + RestApiExceptionFilter, +) export class AiGenerateTextController { constructor( private readonly aiModelRegistryService: AiModelRegistryService, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts index 50489a542c..b34e42f33f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts @@ -53,6 +53,7 @@ import { toLegacyFieldMetadataUpdateResponse, } from 'src/engine/metadata-modules/field-metadata/utils/to-legacy-field-metadata-response.util'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; @Controller('rest/metadata/fields') @UseGuards( @@ -60,7 +61,10 @@ import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-mod WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.DATA_MODEL), ) -@UseFilters(FieldMetadataRestApiExceptionFilter) +@UseFilters( + PermissionsRestApiExceptionFilter, + FieldMetadataRestApiExceptionFilter, +) @UsePipes(new ValidationPipe()) export class FieldMetadataController { constructor( diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts index d74c06d603..22990a3149 100644 --- a/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts @@ -27,11 +27,13 @@ import { FrontComponentExceptionCode, } from 'src/engine/metadata-modules/front-component/front-component.exception'; import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; @Controller('rest/front-components') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, FrontComponentRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts index 2bb13106aa..3ba04fe774 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts @@ -55,6 +55,7 @@ import { toLegacyObjectMetadataListResponse, toLegacyObjectMetadataUpdateResponse, } from 'src/engine/metadata-modules/object-metadata/utils/to-legacy-object-metadata-response.util'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; @Controller('rest/metadata/objects') @UseGuards( @@ -62,7 +63,10 @@ import { WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.DATA_MODEL), ) -@UseFilters(ObjectMetadataRestApiExceptionFilter) +@UseFilters( + PermissionsRestApiExceptionFilter, + ObjectMetadataRestApiExceptionFilter, +) @UsePipes(new ValidationPipe()) export class ObjectMetadataController { constructor( diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts index 6a9f9d965b..45548c427f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts @@ -31,11 +31,13 @@ import { } from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception'; import { PageLayoutTabRestApiExceptionFilter } from 'src/engine/metadata-modules/page-layout-tab/filters/page-layout-tab-rest-api-exception.filter'; import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; @Controller('rest/metadata/pageLayoutTabs') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, PageLayoutTabRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts index 54b0fd642b..14e105094f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts @@ -31,11 +31,13 @@ import { } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception'; import { PageLayoutWidgetRestApiExceptionFilter } from 'src/engine/metadata-modules/page-layout-widget/filters/page-layout-widget-rest-api-exception.filter'; import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; @Controller('rest/metadata/pageLayoutWidgets') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, PageLayoutWidgetRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts index 5282c8fd0e..bb902c02be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts @@ -26,11 +26,13 @@ import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; import { PageLayoutRestApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/filters/page-layout-rest-api-exception.filter'; import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; @Controller('rest/metadata/pageLayouts') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, PageLayoutRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/__tests__/permission-rest-api-exception-code-to-http-status.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/__tests__/permission-rest-api-exception-code-to-http-status.util.spec.ts new file mode 100644 index 0000000000..393623a749 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/__tests__/permission-rest-api-exception-code-to-http-status.util.spec.ts @@ -0,0 +1,76 @@ +import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { permissionRestApiExceptionCodeToHttpStatus } from 'src/engine/metadata-modules/permissions/utils/permission-rest-api-exception-code-to-http-status.util'; + +describe('permissionRestApiExceptionCodeToHttpStatus', () => { + it('should return 403 for PERMISSION_DENIED', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.PERMISSION_DENIED, + ), + ).toBe(403); + }); + + it('should return 403 for role-related forbidden cases', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE, + ), + ).toBe(403); + }); + + it('should return 403 for NO_AUTHENTICATION_CONTEXT', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT, + ), + ).toBe(403); + }); + + it('should return 400 for INVALID_ARG', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.INVALID_ARG, + ), + ).toBe(400); + }); + + it('should return 400 for permission validation errors', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED, + ), + ).toBe(400); + }); + + it('should return 404 for ROLE_NOT_FOUND', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.ROLE_NOT_FOUND, + ), + ).toBe(404); + }); + + it('should return 404 for metadata not found cases', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, + ), + ).toBe(404); + }); + + it('should return 500 for METHOD_NOT_ALLOWED', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.METHOD_NOT_ALLOWED, + ), + ).toBe(500); + }); + + it('should return 500 for internal error codes', () => { + expect( + permissionRestApiExceptionCodeToHttpStatus( + PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND, + ), + ).toBe(500); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-rest-api-exception-code-to-http-status.util.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-rest-api-exception-code-to-http-status.util.ts new file mode 100644 index 0000000000..8f21a3a0a5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-rest-api-exception-code-to-http-status.util.ts @@ -0,0 +1,61 @@ +import { assertUnreachable } from 'twenty-shared/utils'; + +import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception'; + +export const permissionRestApiExceptionCodeToHttpStatus = ( + code: PermissionsExceptionCode, +): number => { + switch (code) { + case PermissionsExceptionCode.PERMISSION_DENIED: + case PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT: + case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS: + case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN: + case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE: + case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER: + case PermissionsExceptionCode.ROLE_NOT_EDITABLE: + case PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT: + case PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT: + return 403; + case PermissionsExceptionCode.INVALID_ARG: + case PermissionsExceptionCode.INVALID_SETTING: + case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT: + case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION: + case PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED: + case PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT: + case PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT: + case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED: + case PermissionsExceptionCode.ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET: + case PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS: + return 400; + case PermissionsExceptionCode.ROLE_NOT_FOUND: + case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND: + case PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND: + case PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND: + case PermissionsExceptionCode.PERMISSION_NOT_FOUND: + return 404; + case PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED: + case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND: + case PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH: + case PermissionsExceptionCode.TOO_MANY_ADMIN_CANDIDATES: + case PermissionsExceptionCode.USER_WORKSPACE_ALREADY_HAS_ROLE: + case PermissionsExceptionCode.ADMIN_ROLE_NOT_FOUND: + case PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED: + case PermissionsExceptionCode.WORKSPACE_MEMBER_NOT_FOUND: + case PermissionsExceptionCode.UNKNOWN_OPERATION_NAME: + case PermissionsExceptionCode.UNKNOWN_REQUIRED_PERMISSION: + case PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE: + case PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE: + case PermissionsExceptionCode.METHOD_NOT_ALLOWED: + case PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED: + case PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND: + case PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND: + case PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED: + case PermissionsExceptionCode.COMPOSITE_TYPE_NOT_FOUND: + case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND: + case PermissionsExceptionCode.APPLICATION_ROLE_NOT_FOUND: + case PermissionsExceptionCode.ROLE_BELONGS_TO_ANOTHER_APPLICATION: + return 500; + default: + return assertUnreachable(code); + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter.ts new file mode 100644 index 0000000000..151212be6d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter.ts @@ -0,0 +1,31 @@ +import { + type ArgumentsHost, + Catch, + type ExceptionFilter, + Injectable, +} from '@nestjs/common'; + +import { type Response } from 'express'; + +import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; +import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { permissionRestApiExceptionCodeToHttpStatus } from 'src/engine/metadata-modules/permissions/utils/permission-rest-api-exception-code-to-http-status.util'; +import { type CustomException } from 'src/utils/custom-exception'; + +@Injectable() +@Catch(PermissionsException) +export class PermissionsRestApiExceptionFilter implements ExceptionFilter { + constructor( + private readonly httpExceptionHandlerService: HttpExceptionHandlerService, + ) {} + + catch(exception: PermissionsException, host: ArgumentsHost) { + const response = host.switchToHttp().getResponse(); + + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + permissionRestApiExceptionCodeToHttpStatus(exception.code), + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts index 0df3400bed..cb9cf21d17 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts @@ -18,6 +18,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-rest-api-exception.filter'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input'; import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input'; import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto'; @@ -38,6 +39,7 @@ import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/works @Controller('rest/metadata/viewFields') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, ViewFieldRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts index c0efac25a5..c3931e7fc0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts @@ -18,6 +18,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-rest-api-exception.filter'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { type CreateViewFilterGroupInput } from 'src/engine/metadata-modules/view-filter-group/dtos/inputs/create-view-filter-group.input'; import { type UpdateViewFilterGroupInput } from 'src/engine/metadata-modules/view-filter-group/dtos/inputs/update-view-filter-group.input'; import { type ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto'; @@ -38,6 +39,7 @@ import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/works @Controller('rest/metadata/viewFilterGroups') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, ViewFilterGroupRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts index 37670a45a1..0859ed8fe3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts @@ -18,6 +18,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-rest-api-exception.filter'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { CreateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/create-view-filter.input'; import { UpdateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/update-view-filter.input'; import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto'; @@ -38,6 +39,7 @@ import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/works @Controller('rest/metadata/viewFilters') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, ViewFilterRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts index 59e11c29c4..e912d41eef 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts @@ -18,6 +18,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-rest-api-exception.filter'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input'; import { UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input'; import { type ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto'; @@ -38,6 +39,7 @@ import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/works @Controller('rest/metadata/viewGroups') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, ViewGroupRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts index 8cc314a1f5..5da00cd433 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts @@ -18,6 +18,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-rest-api-exception.filter'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { CreateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-sort-permission.guard'; import { DeleteViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-sort-permission.guard'; import { UpdateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-sort-permission.guard'; @@ -38,6 +39,7 @@ import { ViewSortDirection } from 'twenty-shared/types'; @Controller('rest/metadata/viewSorts') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, ViewSortRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts index 9d1aefe3c6..eb71df7b81 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts @@ -41,11 +41,13 @@ import { import { ViewRestApiExceptionFilter } from 'src/engine/metadata-modules/view/filters/view-rest-api-exception.filter'; import { ViewService } from 'src/engine/metadata-modules/view/services/view.service'; import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-rest-api-exception.filter'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; @Controller('rest/metadata/views') @UseGuards(WorkspaceAuthGuard) @UseFilters( + PermissionsRestApiExceptionFilter, ViewRestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts index b817a3d2bb..5758d50eec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts @@ -19,6 +19,7 @@ import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-rest-api-exception.filter'; +import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/create-webhook.input'; import { UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input'; import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto'; @@ -32,6 +33,7 @@ import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/works SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS), ) @UseFilters( + PermissionsRestApiExceptionFilter, RestApiExceptionFilter, FlatEntityMapsRestApiExceptionFilter, WorkspaceMigrationRunnerRestApiExceptionFilter,