From a3d73740a154eab0360f03d7d4b894894d00488d Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 5 Jun 2026 16:45:17 +0200 Subject: [PATCH] fix(server): guard role-permission cache against stripped permissionFlag relation during upgrade (#21257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Self-hosted upgrades that jump versions (e.g. `2.4 → 2.7/2.9`) abort with: ``` TypeError: Cannot read properties of undefined (reading 'universalIdentifier') at WorkspaceRolesPermissionsCacheService.hasSettingsGatedObjectPermissions at WorkspaceRolesPermissionsCacheService.computeForCache at WorkspaceCacheService.recomputeDataFromProvider ``` Reported in #20841 (Failure #2). The sequence aborts mid-upgrade and leaves the DB in a half-migrated state. ## Root cause The per-workspace **cache recompute runs at a `2.5.0` workspace step — before the `2.6` schema migrations apply**. At that cursor: - `RolePermissionFlagEntity.permissionFlag` is `@WasIntroducedInUpgrade('2.6.0_LinkRolePermissionFlagToPermissionFlag…')`, so `UpgradeAwareRepositoryProxy` **strips the relation** (`[upgrade-proxy] strip relation RolePermissionFlagEntity.permissionFlag` in the logs) → `permissionFlag` is `undefined`. - `hasSettingsGatedObjectPermissions()` then does an **unguarded** `rolePermissionFlag.permissionFlag.universalIdentifier` → throws. The crash only manifests when a workspace has **≥1 `rolePermissionFlag` row** (custom roles with gated settings perms / SDK `defineRole`). A vanilla seed has an empty table, so `.find()` over `[]` never dereferences anything — which is why it didn't reproduce on a clean instance. A null-safe fallback to the legacy `flag` column used to exist here; it was dropped in #20730. ## Fix Resolve the flag's universal identifier through a small helper that falls back to the legacy `flag` column (only removed in `2.7.0`) when the relation is unavailable: ```ts private getRolePermissionFlagUniversalIdentifier( rolePermissionFlag: RolePermissionFlagEntity, ): string { // The `permissionFlag` relation is stripped during upgrades until the 2.6.0 // cursor (@WasIntroducedInUpgrade), so fall back to the legacy `flag` column. return ( rolePermissionFlag.permissionFlag?.universalIdentifier ?? SystemPermissionFlag[rolePermissionFlag.flag] ); } ``` `SystemPermissionFlag[flag]` yields the same UUID the relation would, so the comparison stays in a single space and the computed permission is exact (not an over-grant). Correct at every transitional cursor: pre-`2.6` (relation stripped → use `flag`), `2.6` (both present → relation wins), post-`2.7` (`flag` removed → relation wins). ## Reproduction & validation Locally jumped a real `2.4.0` DB → `v2.9.0` build via `yarn command:prod upgrade`: | Scenario | Result | | --- | --- | | Empty `permissionFlag` (vanilla seed) | passes (no crash) | | **+1 flag row**, current code | `TypeError … universalIdentifier` → **3 succeeded, 1 failed** | | Same fixture, **this fix** | **16 succeeded, 0 failed**, DB fully migrated to 2.9.0 | `nx typecheck twenty-server` clean; existing cache-service unit tests pass; app boots on the upgraded DB. ## Scope / follow-up This fixes **Failure #2**. **Failure #1** in the same issue (`viewFilter.relationTargetFieldMetadataId` selected before its column exists) is a separate instance of the same theme — cache recompute reading "future" schema before migrations run — and is worth a follow-up. A more durable systemic fix would defer the workspace cache recompute until after all schema-adding migrations; this PR is the low-risk, backport-friendly fix for the immediate breakage. > Note: an earlier bot branch (`sonarly-39738-fixupgrade-guard-role-permission-flag-relation`) proposed the same fallback inline. This PR supersedes it with a named helper + a focused comment. Fixes #20841 Co-authored-by: Claude Opus 4.8 --- .../workspace-roles-permissions-cache.service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts index 582bbfad28..6539aeda43 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts @@ -279,11 +279,22 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide const hasPermissionFromSettingPermissions = isDefined( rolePermissionFlags.find( (rolePermissionFlag) => - rolePermissionFlag.permissionFlag.universalIdentifier === + this.getRolePermissionFlagUniversalIdentifier(rolePermissionFlag) === permissionFlagUniversalIdentifier, ), ); return hasPermissionFromRole || hasPermissionFromSettingPermissions; } + + private getRolePermissionFlagUniversalIdentifier( + rolePermissionFlag: RolePermissionFlagEntity, + ): string { + // The `permissionFlag` relation is stripped during upgrades until the 2.6.0 + // cursor (@WasIntroducedInUpgrade), so fall back to the legacy `flag` column. + return ( + rolePermissionFlag.permissionFlag?.universalIdentifier ?? + SystemPermissionFlag[rolePermissionFlag.flag] + ); + } }