fix(server): allow app-defined permission flags to be referenced by a role in the same sync (#21742)
## Context When an application defined custom permission flags and a role referencing them in the same sync, installation failed, first at validation (Permission flag not found) and then at execution (Migration action 'create' for 'rolePermissionFlag' failed). Root cause: both the migration builder order and the runner execution order processed rolePermissionFlag before permissionFlag, so the role's flag assignments were validated/inserted before the flags they reference existed. ## Changes - Builder order: run the permissionFlag builder before rolePermissionFlag so newly created flags are visible in the optimistic maps when assignments are validated. - Execution order: order the permission-flag actions so definitions are created before assignments, and assignments deleted before definitions, keeping the FK satisfied in both directions. - In-use check: move the "flag still assigned to a role" guard out of the per-entity deletion validator (order-dependent, false-positived when a flag and its assignments were deleted together) into a new order-independent validatePermissionFlagNotInUseCrossEntity (aligned with existing validateObjectMetadataCrossEntity, validateViewFieldLabelIdentifierCrossEntity, ...), run after all builders against the migration's final state. This fixes both the create path (define flag + reference it in one sync) and the teardown path (delete flag + its assignments in one sync). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21742?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+4
-4
@@ -228,14 +228,14 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
ALL_METADATA_NAME.fieldPermission,
|
||||
workspaceMigrationFieldPermissionActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.rolePermissionFlag,
|
||||
workspaceMigrationRolePermissionFlagActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.permissionFlag,
|
||||
workspaceMigrationPermissionFlagActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.rolePermissionFlag,
|
||||
workspaceMigrationRolePermissionFlagActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.roleTarget,
|
||||
workspaceMigrationRoleTargetActionsBuilderService,
|
||||
|
||||
+3
-6
@@ -71,16 +71,13 @@ export const computeOrderedMigrationActions = (
|
||||
...aggregatedOrchestratorActionsReport.fieldPermission.update,
|
||||
///
|
||||
|
||||
// Permission flags
|
||||
// Permission flag definitions and their role assignments.
|
||||
...aggregatedOrchestratorActionsReport.rolePermissionFlag.delete,
|
||||
...aggregatedOrchestratorActionsReport.rolePermissionFlag.create,
|
||||
...aggregatedOrchestratorActionsReport.rolePermissionFlag.update,
|
||||
///
|
||||
|
||||
// Permission flag definitions
|
||||
...aggregatedOrchestratorActionsReport.permissionFlag.delete,
|
||||
...aggregatedOrchestratorActionsReport.permissionFlag.create,
|
||||
...aggregatedOrchestratorActionsReport.rolePermissionFlag.create,
|
||||
...aggregatedOrchestratorActionsReport.permissionFlag.update,
|
||||
...aggregatedOrchestratorActionsReport.rolePermissionFlag.update,
|
||||
///
|
||||
|
||||
// Agents
|
||||
|
||||
+8
@@ -1,4 +1,5 @@
|
||||
import { validateObjectMetadataCrossEntity } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-cross-entity.util';
|
||||
import { validatePermissionFlagNotInUseCrossEntity } from 'src/engine/metadata-modules/flat-permission-flag/validators/utils/validate-permission-flag-not-in-use-cross-entity.util';
|
||||
import { validateViewFieldLabelIdentifierCrossEntity } from 'src/engine/metadata-modules/flat-view-field/validators/utils/validate-view-field-label-identifier-cross-entity.util';
|
||||
import {
|
||||
type OrchestratorActionsReport,
|
||||
@@ -32,8 +33,15 @@ export const crossEntityTransversalValidation = ({
|
||||
preDeletionFlatViewFieldMaps,
|
||||
});
|
||||
|
||||
const { permissionFlag } = validatePermissionFlagNotInUseCrossEntity({
|
||||
optimisticUniversalFlatMaps,
|
||||
deletedPermissionFlagActions:
|
||||
orchestratorActionsReport.permissionFlag.delete,
|
||||
});
|
||||
|
||||
crossEntityFailureReport.objectMetadata.push(...objectMetadata);
|
||||
crossEntityFailureReport.viewField.push(...viewField);
|
||||
crossEntityFailureReport.permissionFlag.push(...permissionFlag);
|
||||
|
||||
validateUniversalIdentifierCrossEntityUniquenessThroughReportMutation({
|
||||
optimisticUniversalFlatMaps,
|
||||
|
||||
-55
@@ -1,6 +1,5 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
@@ -38,23 +37,6 @@ const buildEmptyRolePermissionFlagMaps =
|
||||
(): FlatEntityMaps<FlatRolePermissionFlag> =>
|
||||
createEmptyFlatEntityMaps() as unknown as FlatEntityMaps<FlatRolePermissionFlag>;
|
||||
|
||||
const buildFlatRolePermissionFlag = (
|
||||
overrides: Partial<FlatRolePermissionFlag> = {},
|
||||
): FlatRolePermissionFlag =>
|
||||
({
|
||||
id: '00000000-0000-0000-0000-000000000101',
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000101',
|
||||
permissionFlagId: '00000000-0000-0000-0000-000000000001',
|
||||
permissionFlagUniversalIdentifier: '00000000-0000-0000-0000-000000000001',
|
||||
roleUniversalIdentifier: '00000000-0000-0000-0000-000000000201',
|
||||
workspaceId: 'workspace-id',
|
||||
applicationId: '00000000-0000-0000-0000-000000000aaa',
|
||||
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000aaa',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
}) as unknown as FlatRolePermissionFlag;
|
||||
|
||||
const buildArgs = (
|
||||
flatEntityToValidate: FlatPermissionFlag,
|
||||
optimisticMaps: FlatEntityMaps<FlatPermissionFlag> = buildEmptyMaps(),
|
||||
@@ -335,43 +317,6 @@ describe('FlatPermissionFlagValidatorService', () => {
|
||||
PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects deleting a definition while roles still grant its key', () => {
|
||||
const existing = buildFlatDefinition({
|
||||
key: PermissionFlagType.WORKSPACE,
|
||||
});
|
||||
const optimisticMaps = buildEmptyMaps();
|
||||
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
|
||||
existing;
|
||||
|
||||
const rolePermissionFlagMaps = buildEmptyRolePermissionFlagMaps();
|
||||
const rolePermissionFlag = buildFlatRolePermissionFlag({
|
||||
permissionFlagId: existing.id,
|
||||
permissionFlagUniversalIdentifier: existing.universalIdentifier,
|
||||
});
|
||||
rolePermissionFlagMaps.byUniversalIdentifier[
|
||||
rolePermissionFlag.universalIdentifier
|
||||
] = rolePermissionFlag;
|
||||
|
||||
const result = service.validateFlatPermissionFlagDeletion({
|
||||
flatEntityToValidate: existing,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatPermissionFlagMaps: optimisticMaps,
|
||||
flatRolePermissionFlagMaps: rolePermissionFlagMaps,
|
||||
},
|
||||
buildOptions: {
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
'00000000-0000-0000-0000-000000000aaa',
|
||||
},
|
||||
} as unknown as Parameters<
|
||||
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
|
||||
>[0]);
|
||||
|
||||
expect(result.errors.map((error) => error.code)).toEqual([
|
||||
PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ALL_METADATA_NAME is referenced as a sanity check the metadata name exists
|
||||
|
||||
-18
@@ -166,7 +166,6 @@ export class FlatPermissionFlagValidatorService {
|
||||
flatEntityToValidate: { universalIdentifier },
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps,
|
||||
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
|
||||
},
|
||||
buildOptions,
|
||||
}: UniversalFlatEntityValidationArgs<
|
||||
@@ -209,23 +208,6 @@ export class FlatPermissionFlagValidatorService {
|
||||
});
|
||||
}
|
||||
|
||||
const isPermissionFlagInUse = Object.values(
|
||||
optimisticFlatRolePermissionFlagMaps.byUniversalIdentifier,
|
||||
).some(
|
||||
(rolePermissionFlag) =>
|
||||
isDefined(rolePermissionFlag) &&
|
||||
rolePermissionFlag.permissionFlagUniversalIdentifier ===
|
||||
existing.universalIdentifier,
|
||||
);
|
||||
|
||||
if (isPermissionFlagInUse) {
|
||||
validationResult.errors.push({
|
||||
code: PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE,
|
||||
message: t`Permission flag definition with key ${existing.key} is still assigned to a role`,
|
||||
userFriendlyMessage: msg`Remove this permission from all roles before deleting it`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user