Fix TWENTY-SERVER-60Y: register permissions exception filter globally (#23104)

## Context

Sentry
[TWENTY-SERVER-60Y](https://twenty-v7.sentry.io/issues/6633503406)
("Permission Denied: Entity performing the request does not have
permission") has ~12.9k occurrences / 151 users. It groups by the shared
throw site `settings-permission.guard.ts:57`, so it's a **catch-all
bucket** for settings-permission denials across many resolvers, not a
single operation. Sampled events include `findOneApplication` (app
runtimes reading their own `applicationVariables`),
`uploadFilesFieldFileByUniversalIdentifier`,
`UpdatePageLayoutWithTabsAndWidgets`, `CreateFileUpload`, etc.

## Root cause

Settings-permission denials are only converted to a client-appropriate
`FORBIDDEN` when a resolver manually attaches
`PermissionsGraphqlApiExceptionFilter`. **28** GraphQL resolvers do;
**~35** guarded resolvers do not. On those, the guard-thrown
`PermissionsException` (a `CustomException`, not a `BaseGraphQLError`)
falls through to the Yoga error hook, is serialized as
`INTERNAL_SERVER_ERROR`, and `shouldCaptureException` reports it to
Sentry as a 500-class error.

REST is not affected: every `SettingsPermissionGuard` controller already
carries `PermissionsRestApiExceptionFilter` (15/15).

## Fix

Register `PermissionsGraphqlApiExceptionFilter` globally via
`APP_FILTER` in the core and metadata engine modules, mirroring the
existing global GraphQL filters `BillingGraphqlApiExceptionFilter` and
`FlatEntityMapsGraphqlApiExceptionFilter`. The filter is guarded on the
GraphQL context (`host.getType()`), so REST keeps its existing
per-controller filter untouched and no request-scoped dependency is
pulled into a global provider.

Every settings-guarded resolver now returns `FORBIDDEN` for denials,
which is both the correct client error code and excluded from Sentry.
Existing per-resolver
`@UseFilters(PermissionsGraphqlApiExceptionFilter)` entries remain valid
(handler-scoped takes precedence, identical result); collapsing them
into the global registration is a possible follow-up.

## Test

Added a case to `granular-settings-permissions.integration-spec.ts`: a
member without the `APPLICATIONS` flag calling
`findOneApplication{applicationVariables{key value}}` (the exact Sentry
query, and a resolver with **no** resolver-scoped filter) must receive
`FORBIDDEN`, exercising the global filter.

Verified against the local test DB:
- With the global filter: passes (`FORBIDDEN`).
- Without it: fails with `INTERNAL_SERVER_ERROR`, reproducing the leak.
- The existing roles / workspace-members / api-keys denial tests
(per-resolver filters) still pass, confirming no precedence conflict and
no boot issue from dual `APP_FILTER` registration.

Fixes TWENTY-SERVER-60Y
This commit is contained in:
Thomas Trompette
2026-07-22 12:10:49 +02:00
committed by GitHub
parent 623aac5a56
commit 25f28ee299
4 changed files with 49 additions and 2 deletions
@@ -13,6 +13,7 @@ import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/conn
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { FlatEntityMapsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-graphql-api-exception.filter';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
@@ -69,6 +70,10 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
provide: APP_FILTER,
useClass: FlatEntityMapsGraphqlApiExceptionFilter,
},
{
provide: APP_FILTER,
useClass: PermissionsGraphqlApiExceptionFilter,
},
],
exports: [
FieldMetadataModule,