[1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)

Split of #20377.

## Summary

This PR separates available permission flags from per-role permission
flag grants.

Previously, `core.permissionFlag` stored the role assignment directly:
`roleId + flag`. This PR renames that legacy grant table to
`core.rolePermissionFlag`, then recreates `core.permissionFlag` as the
catalog of available permission flags.

## What changed

- Rename the existing `core.permissionFlag` grant table to
`core.rolePermissionFlag`.
- Add the new syncable `core.permissionFlag` catalog entity with key,
label, description, icon, permission type, relevance flags, and
custom/standard metadata.
- Add stable `SystemPermissionFlag` universal identifiers for the
built-in `PermissionFlagType` values.
- Seed the standard permission flags for every workspace under the
Twenty standard application.
- Backfill existing role grants:
  - create missing catalog rows for existing grant keys,
  - add `rolePermissionFlag.permissionFlagId`,
- migrate grants from the old string `flag` column to the new catalog
FK,
- replace the old `(flag, roleId)` uniqueness with `(permissionFlagId,
roleId)`.
- Rewire role permission flag caches, permission checks, role DTO
mapping, and `upsertPermissionFlags` to resolve through the catalog.
- Keep the existing public role permission API shape: product/app
surfaces still talk about `permissionFlags` and return `{ id, roleId,
flag }`.
- Update metadata flat-entity machinery, migration builders, validators,
action handlers, snapshots, generated schemas, docs, and app fixtures
for the new `permissionFlag` / `rolePermissionFlag` split.

## Behavior after this PR

- Existing permission flag grants keep working.
- Existing GraphQL role permission flows keep the same public naming.
- Standard permission flags are represented as catalog rows.
- Permission checks now compare grants through catalog universal
identifiers instead of the legacy `flag` column.
- Workspace deletion cleanup now verifies both `permissionFlag` and
`rolePermissionFlag`.

## What is not in this PR

- Public GraphQL CRUD for custom permission flags.
- App manifest support for declaring new custom permission flags.
- Frontend UI for creating or assigning custom permission flags beyond
the existing role permission flow.

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
nitin
2026-05-18 17:27:47 +05:30
committed by GitHub
parent 01535a3b3e
commit db0547f503
132 changed files with 3663 additions and 719 deletions
@@ -229,8 +229,11 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
},
"permissionFlag": {
"propertiesToCompare": [
"flag",
"roleUniversalIdentifier",
"key",
"label",
"description",
"icon",
"permissionType",
],
"propertiesToStringify": [],
},
@@ -251,6 +254,14 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
],
"propertiesToStringify": [],
},
"rolePermissionFlag": {
"propertiesToCompare": [
"permissionFlagUniversalIdentifier",
"roleUniversalIdentifier",
"flag",
],
"propertiesToStringify": [],
},
"roleTarget": {
"propertiesToCompare": [
"roleUniversalIdentifier",
@@ -1186,17 +1186,59 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: 'pageLayoutUniversalIdentifier',
},
},
permissionFlag: {
flag: {
rolePermissionFlag: {
permissionFlagId: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
universalProperty: 'permissionFlagUniversalIdentifier',
},
roleId: {
toCompare: true,
toStringify: false,
universalProperty: 'roleUniversalIdentifier',
},
flag: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
updatedAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
},
permissionFlag: {
key: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
label: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
description: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
icon: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
permissionType: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
@@ -171,12 +171,19 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
workspace: null,
application: null,
},
permissionFlag: {
rolePermissionFlag: {
workspace: null,
application: null,
role: {
foreignKey: 'roleId',
},
permissionFlag: {
foreignKey: 'permissionFlagId',
},
},
permissionFlag: {
workspace: null,
application: null,
},
objectPermission: {
workspace: null,
@@ -300,16 +300,27 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
workspace: null,
application: null,
},
permissionFlag: {
rolePermissionFlag: {
workspace: null,
application: null,
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'permissionFlags',
inverseOneToManyProperty: 'rolePermissionFlags',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
permissionFlag: {
metadataName: 'permissionFlag',
foreignKey: 'permissionFlagId',
inverseOneToManyProperty: 'rolePermissionFlags',
isNullable: true,
universalForeignKey: 'permissionFlagUniversalIdentifier',
},
},
permissionFlag: {
workspace: null,
application: null,
},
objectPermission: {
workspace: null,
@@ -17,6 +17,7 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -56,6 +57,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
agent: AgentEntity,
commandMenuItem: CommandMenuItemEntity,
navigationMenuItem: NavigationMenuItemEntity,
rolePermissionFlag: RolePermissionFlagEntity,
permissionFlag: PermissionFlagEntity,
webhook: WebhookEntity,
applicationVariable: ApplicationVariableEntity,
@@ -76,9 +76,13 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
view: true,
pageLayout: true,
},
permissionFlag: {
rolePermissionFlag: {
permissionFlag: true,
role: true,
},
permissionFlag: {
rolePermissionFlag: true,
},
objectPermission: {
role: true,
objectMetadata: true,
@@ -33,6 +33,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
logicFunction: {},
role: {},
roleTarget: {},
rolePermissionFlag: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
@@ -168,11 +168,11 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'objectPermissionUniversalIdentifiers',
},
permissionFlags: {
metadataName: 'permissionFlag',
flatEntityForeignKeyAggregator: 'permissionFlagIds',
rolePermissionFlags: {
metadataName: 'rolePermissionFlag',
flatEntityForeignKeyAggregator: 'rolePermissionFlagIds',
universalFlatEntityForeignKeyAggregator:
'permissionFlagUniversalIdentifiers',
'rolePermissionFlagUniversalIdentifiers',
},
fieldPermissions: {
metadataName: 'fieldPermission',
@@ -194,7 +194,15 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
},
},
roleTarget: {},
permissionFlag: {},
rolePermissionFlag: {},
permissionFlag: {
rolePermissionFlags: {
metadataName: 'rolePermissionFlag',
flatEntityForeignKeyAggregator: 'rolePermissionFlagIds',
universalFlatEntityForeignKeyAggregator:
'rolePermissionFlagUniversalIdentifiers',
},
},
objectPermission: {},
fieldPermission: {},
pageLayout: {
@@ -13,6 +13,7 @@ import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-m
import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service';
import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service';
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
import { WorkspaceFlatRolePermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-role-permission-flag/services/workspace-flat-role-permission-flag-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service';
import { WorkspaceFlatViewFieldGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service';
@@ -32,6 +33,7 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -64,6 +66,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
PageLayoutEntity,
PageLayoutTabEntity,
PageLayoutWidgetEntity,
RolePermissionFlagEntity,
PermissionFlagEntity,
RowLevelPermissionPredicateEntity,
RowLevelPermissionPredicateGroupEntity,
@@ -85,6 +88,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatRolePermissionFlagMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -106,6 +110,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatRolePermissionFlagMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -26,6 +26,8 @@ import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-l
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type FlatRolePermissionFlagMaps } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag-maps.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
@@ -70,6 +72,7 @@ import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/wo
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
import { type UniversalFlatRoleTarget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-target.type';
import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type';
import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type';
@@ -204,6 +207,14 @@ import {
type UniversalDeletePermissionFlagAction,
type UniversalUpdatePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import {
type FlatCreateRolePermissionFlagAction,
type FlatDeleteRolePermissionFlagAction,
type FlatUpdateRolePermissionFlagAction,
type UniversalCreateRolePermissionFlagAction,
type UniversalDeleteRolePermissionFlagAction,
type UniversalUpdateRolePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import {
type FlatCreateRoleTargetAction,
type FlatDeleteRoleTargetAction,
@@ -625,6 +636,22 @@ export type AllFlatEntityTypesByMetadataName = {
universalFlatEntity: UniversalFlatNavigationMenuItem;
entity: NavigationMenuItemEntity;
};
rolePermissionFlag: {
flatEntityMaps: FlatRolePermissionFlagMaps;
universalActions: {
create: UniversalCreateRolePermissionFlagAction;
update: UniversalUpdateRolePermissionFlagAction;
delete: UniversalDeleteRolePermissionFlagAction;
};
flatActions: {
create: FlatCreateRolePermissionFlagAction;
update: FlatUpdateRolePermissionFlagAction;
delete: FlatDeleteRolePermissionFlagAction;
};
flatEntity: FlatRolePermissionFlag;
universalFlatEntity: UniversalFlatRolePermissionFlag;
entity: MetadataEntity<'rolePermissionFlag'>;
};
permissionFlag: {
flatEntityMaps: FlatPermissionFlagMaps;
universalActions: {
@@ -93,7 +93,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
exports[`getMetadataRelatedMetadataNames should return related metadata names for permissionFlag 1`] = `
[
"role",
"rolePermissionFlag",
]
`;
@@ -101,13 +101,20 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
[
"roleTarget",
"objectPermission",
"permissionFlag",
"rolePermissionFlag",
"fieldPermission",
"rowLevelPermissionPredicate",
"rowLevelPermissionPredicateGroup",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for rolePermissionFlag 1`] = `
[
"role",
"permissionFlag",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for roleTarget 1`] = `
[
"role",
@@ -10,10 +10,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewField",
"objectPermission",
"pageLayoutWidget",
"rolePermissionFlag",
"viewSort",
"index",
"pageLayout",
"permissionFlag",
"roleTarget",
"rowLevelPermissionPredicateGroup",
"viewGroup",
@@ -27,6 +27,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewFieldGroup",
"viewFilterGroup",
"webhook",
"permissionFlag",
"view",
"fieldMetadata",
"objectMetadata",
@@ -1,6 +1,8 @@
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
export const FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES = [
'flag',
'roleId',
'label',
'description',
'icon',
'permissionType',
] as const satisfies MetadataEntityPropertyName<'permissionFlag'>[];
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
ApplicationEntity,
PermissionFlagEntity,
RolePermissionFlagEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatPermissionFlagMapCacheService],
exports: [WorkspaceFlatPermissionFlagMapCacheService],
})
export class FlatPermissionFlagModule {}
@@ -8,10 +8,11 @@ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-enti
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { fromPermissionFlagEntityToFlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-permission-flag-entity-to-flat-permission-flag.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -22,46 +23,52 @@ export class WorkspaceFlatPermissionFlagMapCacheService extends WorkspaceCachePr
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatPermissionFlagMaps> {
const [permissionFlags, applications, roles] = await Promise.all([
this.permissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const [permissionFlags, applications, rolePermissionFlags] =
await Promise.all([
this.permissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.rolePermissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const rolePermissionFlagsByPermissionFlagId =
regroupEntitiesByRelatedEntityId<'rolePermissionFlag'>({
entities: rolePermissionFlags,
foreignKey: 'permissionFlagId',
});
const flatPermissionFlagMaps = createEmptyFlatEntityMaps();
for (const permissionFlagEntity of permissionFlags) {
const flatPermissionFlag = fromPermissionFlagEntityToFlatPermissionFlag({
entity: permissionFlagEntity,
for (const definition of permissionFlags) {
const flatDefinition = fromPermissionFlagEntityToFlatPermissionFlag({
entity: {
...definition,
rolePermissionFlags:
rolePermissionFlagsByPermissionFlagId.get(definition.id) ?? [],
},
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatPermissionFlag,
flatEntity: flatDefinition,
flatEntityMapsToMutate: flatPermissionFlagMaps,
});
}
@@ -1,38 +1,44 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type CreatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
export const fromCreatePermissionFlagInputToFlatPermissionFlagToCreate = ({
createPermissionFlagInput,
workspaceId,
flatApplication,
flatRoleMaps,
}: {
createPermissionFlagInput: CreatePermissionFlagInput;
workspaceId: string;
flatApplication: FlatApplication;
} & Pick<AllFlatEntityMaps, 'flatRoleMaps'>): UniversalFlatPermissionFlag & {
id: string;
} => {
const { roleId, flag, universalIdentifier } = createPermissionFlagInput;
}): FlatPermissionFlag => {
const now = new Date().toISOString();
const { roleUniversalIdentifier } = resolveEntityRelationUniversalIdentifiers(
{
metadataName: 'permissionFlag',
foreignKeyValues: { roleId },
flatEntityMaps: { flatRoleMaps },
},
);
const { key, label, description, icon } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
createPermissionFlagInput,
['key', 'label', 'description', 'icon'],
);
const id = createPermissionFlagInput.id ?? v4();
const universalIdentifier =
createPermissionFlagInput.universalIdentifier ?? id;
return {
id: v4(),
flag,
universalIdentifier: universalIdentifier ?? v4(),
id,
universalIdentifier,
key,
label,
description: description ?? null,
icon: icon ?? null,
permissionType: createPermissionFlagInput.permissionType,
workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
roleUniversalIdentifier,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
};
@@ -0,0 +1,31 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
PermissionFlagException,
PermissionFlagExceptionCode,
} from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
export const fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow = ({
flatPermissionFlagMaps,
permissionFlagId,
}: {
flatPermissionFlagMaps: FlatEntityMaps<FlatPermissionFlag>;
permissionFlagId: string;
}): FlatPermissionFlag => {
const existing = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: permissionFlagId,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(existing)) {
throw new PermissionFlagException(
'Permission flag not found',
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
);
}
return existing;
};
@@ -0,0 +1,18 @@
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
export const fromFlatPermissionFlagToPermissionFlagDto = (
flatDefinition: FlatPermissionFlag,
): PermissionFlagDTO => ({
id: flatDefinition.id,
universalIdentifier: flatDefinition.universalIdentifier,
key: flatDefinition.key,
label: flatDefinition.label,
description: flatDefinition.description,
icon: flatDefinition.icon,
permissionType: flatDefinition.permissionType,
workspaceId: flatDefinition.workspaceId,
applicationId: flatDefinition.applicationId,
createdAt: new Date(flatDefinition.createdAt),
updatedAt: new Date(flatDefinition.updatedAt),
});
@@ -11,9 +11,8 @@ import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/type
export const fromPermissionFlagEntityToFlatPermissionFlag = ({
entity: permissionFlagEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'permissionFlag'>): FlatPermissionFlag => {
const permissionFlagEntityWithoutRelations = removePropertiesFromRecord(
const entityWithoutRelations = removePropertiesFromRecord(
permissionFlagEntity,
getMetadataEntityRelationProperties('permissionFlag'),
);
@@ -30,24 +29,18 @@ export const fromPermissionFlagEntityToFlatPermissionFlag = ({
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
permissionFlagEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${permissionFlagEntity.roleId} not found for permissionFlag ${permissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...permissionFlagEntityWithoutRelations,
...entityWithoutRelations,
createdAt: permissionFlagEntity.createdAt.toISOString(),
updatedAt: permissionFlagEntity.updatedAt.toISOString(),
universalIdentifier:
permissionFlagEntityWithoutRelations.universalIdentifier,
universalIdentifier: entityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
rolePermissionFlagIds: permissionFlagEntity.rolePermissionFlags.map(
({ id }) => id,
),
rolePermissionFlagUniversalIdentifiers:
permissionFlagEntity.rolePermissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
};
};
@@ -0,0 +1,42 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-permission-flag/constants/flat-permission-flag-editable-properties.constant';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
PermissionFlagException,
PermissionFlagExceptionCode,
} from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
import { type UpdatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/update-permission-flag.input';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow =
({
flatPermissionFlagMaps,
updatePermissionFlagInput,
}: {
flatPermissionFlagMaps: FlatEntityMaps<FlatPermissionFlag>;
updatePermissionFlagInput: UpdatePermissionFlagInput;
}): FlatPermissionFlag => {
const existing = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updatePermissionFlagInput.id,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(existing)) {
throw new PermissionFlagException(
'Permission flag not found',
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
);
}
return {
...mergeUpdateInExistingRecord({
existing,
properties: [...FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES],
update: updatePermissionFlagInput.update,
}),
updatedAt: new Date().toISOString(),
};
};
@@ -0,0 +1,57 @@
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
STANDARD_PERMISSION_FLAG_DEFINITIONS,
type StandardPermissionFlagDefinition,
} from 'src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant';
const STANDARD_PERMISSION_FLAG_DEFINITION_BY_KEY: Record<
PermissionFlagType,
StandardPermissionFlagDefinition
> = STANDARD_PERMISSION_FLAG_DEFINITIONS.reduce(
(accumulator, definition) => {
accumulator[definition.key] = definition;
return accumulator;
},
{} as Record<PermissionFlagType, StandardPermissionFlagDefinition>,
);
const SYNTHESIZED_TIMESTAMP = new Date(0).toISOString();
// TODO: Remove this once we have a proper permission flag catalog backfilled
export const synthesizeFlatPermissionFlagFromFlag = ({
flag,
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
flag: PermissionFlagType;
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): FlatPermissionFlag => {
const definition = STANDARD_PERMISSION_FLAG_DEFINITION_BY_KEY[flag];
const universalIdentifier = SystemPermissionFlag[flag];
return {
id: universalIdentifier,
universalIdentifier,
workspaceId,
applicationId,
applicationUniversalIdentifier,
key: flag,
label: definition.label,
description: definition.description,
icon: definition.icon,
permissionType: definition.permissionType,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: SYNTHESIZED_TIMESTAMP,
updatedAt: SYNTHESIZED_TIMESTAMP,
};
};
@@ -0,0 +1,86 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatRolePermissionFlagMaps } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag-maps.type';
import { fromRolePermissionFlagEntityToFlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/utils/from-role-permission-flag-entity-to-flat-role-permission-flag.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatRolePermissionFlagMaps')
export class WorkspaceFlatRolePermissionFlagMapCacheService extends WorkspaceCacheProvider<FlatRolePermissionFlagMaps> {
constructor(
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatRolePermissionFlagMaps> {
const [rolePermissionFlags, applications, roles, permissionFlags] =
await Promise.all([
this.rolePermissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.permissionFlagRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const permissionFlagIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(permissionFlags);
const flatRolePermissionFlagMaps = createEmptyFlatEntityMaps();
for (const rolePermissionFlagEntity of rolePermissionFlags) {
const flatRolePermissionFlag =
fromRolePermissionFlagEntityToFlatRolePermissionFlag({
entity: rolePermissionFlagEntity,
applicationIdToUniversalIdentifierMap,
permissionFlagIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRolePermissionFlag,
flatEntityMapsToMutate: flatRolePermissionFlagMaps,
});
}
return flatRolePermissionFlagMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
export type FlatRolePermissionFlagMaps = FlatEntityMaps<FlatRolePermissionFlag>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
export type FlatRolePermissionFlag = FlatEntityFrom<RolePermissionFlagEntity>;
@@ -0,0 +1,45 @@
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { type CreateRolePermissionFlagInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/create-role-permission-flag.input';
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
export const fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate =
({
createRolePermissionFlagInput,
flatApplication,
flatPermissionFlagMaps,
flatRoleMaps,
}: {
createRolePermissionFlagInput: CreateRolePermissionFlagInput;
flatApplication: FlatApplication;
} & Pick<
AllFlatEntityMaps,
'flatPermissionFlagMaps' | 'flatRoleMaps'
>): UniversalFlatRolePermissionFlag & {
id: string;
} => {
const { roleId, permissionFlagId, flag, universalIdentifier } =
createRolePermissionFlagInput;
const now = new Date().toISOString();
const { permissionFlagUniversalIdentifier, roleUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'rolePermissionFlag',
foreignKeyValues: { permissionFlagId, roleId },
flatEntityMaps: { flatPermissionFlagMaps, flatRoleMaps },
});
return {
id: v4(),
universalIdentifier: universalIdentifier ?? v4(),
applicationUniversalIdentifier: flatApplication.universalIdentifier,
permissionFlagUniversalIdentifier,
roleUniversalIdentifier,
flag,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,71 @@
import { SystemPermissionFlag } from 'twenty-shared/constants';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromRolePermissionFlagEntityToFlatRolePermissionFlag = ({
entity: rolePermissionFlagEntity,
applicationIdToUniversalIdentifierMap,
permissionFlagIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'rolePermissionFlag'>): FlatRolePermissionFlag => {
const rolePermissionFlagEntityWithoutRelations = removePropertiesFromRecord(
rolePermissionFlagEntity,
getMetadataEntityRelationProperties('rolePermissionFlag'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${rolePermissionFlagEntity.applicationId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${rolePermissionFlagEntity.roleId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const permissionFlagUniversalIdentifier = isDefined(
rolePermissionFlagEntity.permissionFlagId,
)
? permissionFlagIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.permissionFlagId,
)
: SystemPermissionFlag[rolePermissionFlagEntity.flag];
if (!isDefined(permissionFlagUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PermissionFlag with id ${rolePermissionFlagEntity.permissionFlagId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...rolePermissionFlagEntityWithoutRelations,
createdAt: rolePermissionFlagEntity.createdAt.toISOString(),
updatedAt: rolePermissionFlagEntity.updatedAt.toISOString(),
universalIdentifier:
rolePermissionFlagEntityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
permissionFlagUniversalIdentifier,
roleUniversalIdentifier,
};
};
@@ -2,8 +2,8 @@ import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/typ
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export type FlatRole = FlatEntityFrom<RoleEntity> & {
// TODO remove once objectPermission permissionFlag fieldPermission have been migrated to v2
// TODO remove once objectPermission rolePermissionFlag fieldPermission have been migrated to v2
objectPermissionIds: string[];
permissionFlagIds: string[];
rolePermissionFlagIds: string[];
fieldPermissionIds: string[];
};
@@ -54,9 +54,9 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
roleTargetIds: [],
roleTargetUniversalIdentifiers: [],
objectPermissionIds: [],
permissionFlagIds: [],
rolePermissionFlagIds: [],
objectPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
rolePermissionFlagUniversalIdentifiers: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
rowLevelPermissionPredicateIds: [],
@@ -13,10 +13,10 @@ import { type RegroupedEntity } from 'src/engine/workspace-cache/utils/regroup-e
type FromRoleEntityToFlatRoleArgs = {
entity: Omit<
EntityWithRegroupedOneToManyRelations<MetadataEntity<'role'>>,
'objectPermissions' | 'permissionFlags' | 'fieldPermissions'
'objectPermissions' | 'rolePermissionFlags' | 'fieldPermissions'
> & {
objectPermissions: RegroupedEntity[];
permissionFlags: RegroupedEntity[];
rolePermissionFlags: RegroupedEntity[];
fieldPermissions: RegroupedEntity[];
};
} & EntityManyToOneIdByUniversalIdentifierMaps<'role'>;
@@ -57,7 +57,7 @@ export const fromRoleEntityToFlatRole = ({
applicationId: roleEntity.applicationId,
roleTargetIds: roleEntity.roleTargets.map(({ id }) => id),
objectPermissionIds: roleEntity.objectPermissions.map(({ id }) => id),
permissionFlagIds: roleEntity.permissionFlags.map(({ id }) => id),
rolePermissionFlagIds: roleEntity.rolePermissionFlags.map(({ id }) => id),
fieldPermissionIds: roleEntity.fieldPermissions.map(({ id }) => id),
rowLevelPermissionPredicateIds: roleEntity.rowLevelPermissionPredicates.map(
({ id }) => id,
@@ -71,7 +71,7 @@ export const fromRoleEntityToFlatRole = ({
objectPermissionUniversalIdentifiers: roleEntity.objectPermissions.map(
({ universalIdentifier }) => universalIdentifier,
),
permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map(
rolePermissionFlagUniversalIdentifiers: roleEntity.rolePermissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
fieldPermissionUniversalIdentifiers: roleEntity.fieldPermissions.map(
@@ -17,6 +17,7 @@ import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/messag
import { MessageFolderMetadataModule } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.module';
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { MinimalMetadataModule } from 'src/engine/metadata-modules/minimal-metadata/minimal-metadata.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
@@ -47,6 +48,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
WorkspaceMetadataVersionModule,
RoleModule,
PermissionsModule,
PermissionFlagModule,
RouteTriggerModule,
WebhookModule,
ConnectedAccountMetadataModule,
@@ -75,6 +77,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
ViewModule,
RoleModule,
PermissionsModule,
PermissionFlagModule,
WebhookModule,
ConnectedAccountMetadataModule,
MessageChannelMetadataModule,
@@ -0,0 +1,4 @@
export const PERMISSION_FLAG_PERMISSION_TYPES = ['settings', 'tool'] as const;
export type PermissionFlagPermissionType =
(typeof PERMISSION_FLAG_PERMISSION_TYPES)[number];
@@ -0,0 +1,160 @@
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
export type StandardPermissionFlagDefinition = {
key: PermissionFlagType;
universalIdentifier: string;
label: string;
description: string | null;
icon: string | null;
permissionType: PermissionFlagPermissionType;
};
type StandardPermissionFlagMetadata = Pick<
StandardPermissionFlagDefinition,
'label' | 'description' | 'icon'
>;
const STANDARD_PERMISSION_FLAG_METADATA: Record<
PermissionFlagType,
StandardPermissionFlagMetadata
> = {
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: {
label: 'API Keys & Webhooks',
description: 'Manage API keys and webhooks',
icon: 'IconCode',
},
[PermissionFlagType.WORKSPACE]: {
label: 'Workspace',
description: 'Set global workspace preferences',
icon: 'IconSettings',
},
[PermissionFlagType.WORKSPACE_MEMBERS]: {
label: 'Users',
description: 'Add or remove users',
icon: 'IconUsers',
},
[PermissionFlagType.ROLES]: {
label: 'Roles',
description: 'Define user roles and access levels',
icon: 'IconLockOpen',
},
[PermissionFlagType.DATA_MODEL]: {
label: 'Data Model',
description: 'Edit data structure and fields',
icon: 'IconHierarchy',
},
[PermissionFlagType.SECURITY]: {
label: 'Security',
description: 'Manage security policies',
icon: 'IconKey',
},
[PermissionFlagType.WORKFLOWS]: {
label: 'Workflows',
description: 'Manage workflows',
icon: 'IconSettingsAutomation',
},
[PermissionFlagType.IMPERSONATE]: {
label: 'Impersonate',
description: 'Impersonate workspace users',
icon: 'IconSpy',
},
[PermissionFlagType.SSO_BYPASS]: {
label: 'SSO Bypass',
description: 'Enable bypass options',
icon: 'IconShield',
},
[PermissionFlagType.APPLICATIONS]: {
label: 'Applications',
description: 'Install and manage applications',
icon: 'IconApps',
},
[PermissionFlagType.MARKETPLACE_APPS]: {
label: 'Marketplace Apps',
description: 'Browse and install marketplace apps',
icon: 'IconShoppingBag',
},
[PermissionFlagType.LAYOUTS]: {
label: 'Layouts',
description: 'Customize page layouts and UI structure',
icon: 'IconLayoutSidebarRightCollapse',
},
[PermissionFlagType.BILLING]: {
label: 'Billing',
description: 'Manage billing and subscriptions',
icon: 'IconCreditCard',
},
[PermissionFlagType.AI_SETTINGS]: {
label: 'AI',
description: 'Create and configure AI agents',
icon: 'IconSparkles',
},
[PermissionFlagType.AI]: {
label: 'Ask AI',
description: 'Chat with AI agents and use AI features',
icon: 'IconSparkles',
},
[PermissionFlagType.VIEWS]: {
label: 'Manage Views',
description: 'Create, edit, and delete workspace views',
icon: 'IconTable',
},
[PermissionFlagType.UPLOAD_FILE]: {
label: 'Upload Files',
description: 'Allow uploading files and attachments',
icon: 'IconFileUpload',
},
[PermissionFlagType.DOWNLOAD_FILE]: {
label: 'Download Files',
description: 'Allow downloading files and attachments',
icon: 'IconDownload',
},
[PermissionFlagType.SEND_EMAIL_TOOL]: {
label: 'Send Email',
description: 'Send emails via connected accounts',
icon: 'IconMail',
},
[PermissionFlagType.HTTP_REQUEST_TOOL]: {
label: 'HTTP Request',
description: 'Make HTTP requests to external APIs',
icon: 'IconApi',
},
[PermissionFlagType.CODE_INTERPRETER_TOOL]: {
label: 'Code Interpreter',
description: 'Run code to analyze files and data',
icon: 'IconCode',
},
[PermissionFlagType.IMPORT_CSV]: {
label: 'Import CSV',
description: 'Allow importing data from CSV files',
icon: 'IconFileImport',
},
[PermissionFlagType.EXPORT_CSV]: {
label: 'Export CSV',
description: 'Allow exporting data to CSV files',
icon: 'IconFileExport',
},
[PermissionFlagType.CONNECTED_ACCOUNTS]: {
label: 'Sync Account',
description: 'Sync email and calendar accounts',
icon: 'IconAt',
},
[PermissionFlagType.PROFILE_INFORMATION]: {
label: 'Edit Profile',
description: 'Edit own profile information',
icon: 'IconUser',
},
};
export const STANDARD_PERMISSION_FLAG_DEFINITIONS: StandardPermissionFlagDefinition[] =
Object.values(PermissionFlagType).map((key) => ({
key,
universalIdentifier: SystemPermissionFlag[key],
...STANDARD_PERMISSION_FLAG_METADATA[key],
permissionType: TOOL_PERMISSION_FLAGS.includes(key) ? 'tool' : 'settings',
}));
@@ -1,7 +1,52 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { Field, InputType } from '@nestjs/graphql';
export type CreatePermissionFlagInput = {
roleId: string;
flag: PermissionFlagType;
import {
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@InputType()
export class CreatePermissionFlagInput {
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
universalIdentifier?: string;
};
@IsString()
@IsNotEmpty()
@Field()
key: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagPermissionType;
}
@@ -1,17 +1,67 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
IsDateString,
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@ObjectType('PermissionFlag')
export class PermissionFlagDTO {
@Field(() => UUIDScalarType, { nullable: false })
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
roleId: string;
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
universalIdentifier: string;
@Field({ nullable: false })
flag: PermissionFlagType;
@IsString()
@IsNotEmpty()
@Field()
key: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
description: string | null;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
icon: string | null;
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagPermissionType;
@HideField()
workspaceId: string;
@Field(() => UUIDScalarType)
applicationId: string;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -0,0 +1,57 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@InputType()
export class UpdatePermissionFlagInputUpdates {
@IsOptional()
@IsString()
@Field({ nullable: true })
label?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
icon?: string;
@IsOptional()
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String, { nullable: true })
permissionType?: PermissionFlagPermissionType;
}
@InputType()
export class UpdatePermissionFlagInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType, {
description: 'The id of the permission flag definition to update',
})
id: string;
@Type(() => UpdatePermissionFlagInputUpdates)
@ValidateNested()
@Field(() => UpdatePermissionFlagInputUpdates, {
description: 'The fields to update',
})
update: UpdatePermissionFlagInputUpdates;
}
@@ -1,38 +1,46 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('permissionFlag')
@Unique('IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId'])
@Index('IDX_PERMISSION_FLAG_ROLE_ID', ['roleId'])
@Unique('IDX_PERMISSION_FLAG_KEY_WORKSPACE_ID_UNIQUE', ['key', 'workspaceId'])
@Index('IDX_PERMISSION_FLAG_APPLICATION_ID', ['applicationId'])
export class PermissionFlagEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.permissionFlags, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
key: string;
@Column({ nullable: false, type: 'varchar' })
flag: PermissionFlagType;
label: string;
@Column({ nullable: true, type: 'text' })
description: string | null;
@Column({ nullable: true, type: 'varchar' })
icon: string | null;
@Column({ nullable: false, type: 'varchar' })
permissionType: PermissionFlagPermissionType;
@OneToMany(
() => RolePermissionFlagEntity,
(rolePermissionFlag) => rolePermissionFlag.permissionFlag,
)
rolePermissionFlags: Relation<RolePermissionFlagEntity[]>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -0,0 +1,52 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum PermissionFlagExceptionCode {
PERMISSION_FLAG_NOT_FOUND = 'PERMISSION_FLAG_NOT_FOUND',
PERMISSION_FLAG_ALREADY_EXISTS = 'PERMISSION_FLAG_ALREADY_EXISTS',
INVALID_PERMISSION_FLAG_KEY = 'INVALID_PERMISSION_FLAG_KEY',
INVALID_PERMISSION_FLAG_PERMISSION_TYPE = 'INVALID_PERMISSION_FLAG_PERMISSION_TYPE',
PERMISSION_FLAG_KEY_IMMUTABLE = 'PERMISSION_FLAG_KEY_IMMUTABLE',
PERMISSION_FLAG_IS_STANDARD = 'PERMISSION_FLAG_IS_STANDARD',
PERMISSION_FLAG_IN_USE = 'PERMISSION_FLAG_IN_USE',
}
const getPermissionFlagExceptionUserFriendlyMessage = (
code: PermissionFlagExceptionCode,
) => {
switch (code) {
case PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND:
return msg`Permission flag not found.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS:
return msg`Permission flag already exists.`;
case PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_KEY:
return msg`Invalid permission flag key.`;
case PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE:
return msg`Invalid permission flag permission type.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_KEY_IMMUTABLE:
return msg`Permission flag key cannot be changed.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD:
return msg`Standard permission flags cannot be modified.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE:
return msg`Permission flag is still assigned to a role.`;
default:
assertUnreachable(code);
}
};
export class PermissionFlagException extends CustomException<PermissionFlagExceptionCode> {
constructor(
message: string,
code: PermissionFlagExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getPermissionFlagExceptionUserFriendlyMessage(code),
});
}
}
@@ -2,16 +2,20 @@ import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FlatPermissionFlagModule } from 'src/engine/metadata-modules/flat-permission-flag/flat-permission-flag.module';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceCacheModule,
ApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
WorkspaceCacheStorageModule,
ApplicationModule,
FlatPermissionFlagModule,
PermissionsModule,
],
providers: [PermissionFlagService],
exports: [PermissionFlagService],
@@ -1,19 +1,18 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { fromCreatePermissionFlagInputToFlatPermissionFlagToCreate } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-create-permission-flag-input-to-flat-permission-flag-to-create.util';
import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-delete-permission-flag-input-to-flat-permission-flag-or-throw.util';
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
import { fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-update-permission-flag-input-to-flat-permission-flag-to-update-or-throw.util';
import { type CreatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { type UpdatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/update-permission-flag.input';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@@ -25,115 +24,8 @@ export class PermissionFlagService {
private readonly applicationService: ApplicationService,
) {}
public async upsertPermissionFlags({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertPermissionFlagsInput;
}): Promise<FlatPermissionFlag[]> {
const { flatPermissionFlagMaps, flatRoleMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps', 'flatRoleMaps'],
},
);
const roleUniversalId = flatRoleMaps.universalIdentifierById[input.roleId];
const role = isDefined(roleUniversalId)
? flatRoleMaps.byUniversalIdentifier[roleUniversalId]
: undefined;
if (!isDefined(role)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are trying to modify could not be found.`,
},
);
}
const invalidFlags = input.permissionFlagKeys.filter(
(flag) => !Object.values(PermissionFlagType).includes(flag),
);
if (invalidFlags.length > 0) {
throw new PermissionsException(
`${PermissionsExceptionMessage.INVALID_SETTING}: ${invalidFlags.join(', ')}`,
PermissionsExceptionCode.INVALID_SETTING,
{
userFriendlyMessage: msg`Some of the permissions you selected are not valid. Please try again with valid permission settings.`,
},
);
}
const roleUniversalIdentifier = role.universalIdentifier;
const currentPermissionFlagsForRole = Object.values(
flatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatPermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
const inputSet = new Set(input.permissionFlagKeys);
const existingSet = new Set(
currentPermissionFlagsForRole.map((pf) => pf.flag),
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatEntityToCreate = input.permissionFlagKeys
.filter((flag) => !existingSet.has(flag))
.map((flag) =>
fromCreatePermissionFlagInputToFlatPermissionFlagToCreate({
createPermissionFlagInput: { roleId: input.roleId, flag },
flatApplication: workspaceCustomFlatApplication,
flatRoleMaps,
}),
);
const flatEntityToDelete = currentPermissionFlagsForRole.filter(
(pf) => !inputSet.has(pf.flag),
);
if (flatEntityToCreate.length === 0 && flatEntityToDelete.length === 0) {
const unchanged = currentPermissionFlagsForRole.filter((pf) =>
inputSet.has(pf.flag),
);
return unchanged;
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate,
flatEntityToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting permission flags',
);
}
const { flatPermissionFlagMaps: freshFlatPermissionFlagMaps } =
async findAll(workspaceId: string): Promise<PermissionFlagDTO[]> {
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -141,13 +33,196 @@ export class PermissionFlagService {
},
);
const resultFlags = Object.values(
freshFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatPermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
return Object.values(flatPermissionFlagMaps.byUniversalIdentifier)
.filter(isDefined)
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
.map(fromFlatPermissionFlagToPermissionFlagDto);
}
return resultFlags;
async findById(
id: string,
workspaceId: string,
): Promise<PermissionFlagDTO | null> {
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatPermissionFlag = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(flatPermissionFlag)) {
return null;
}
return fromFlatPermissionFlagToPermissionFlagDto(flatPermissionFlag);
}
async create(
input: CreatePermissionFlagInput,
workspaceId: string,
): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatToCreate =
fromCreatePermissionFlagInputToFlatPermissionFlagToCreate({
createPermissionFlagInput: input,
workspaceId,
flatApplication: workspaceCustomFlatApplication,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [flatToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while creating permission flag',
);
}
const { flatPermissionFlagMaps: recomputedMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return fromFlatPermissionFlagToPermissionFlagDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatToCreate.id,
flatEntityMaps: recomputedMaps,
}),
);
}
async update(
input: UpdatePermissionFlagInput,
workspaceId: string,
): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatPermissionFlagMaps: existingMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatToUpdate =
fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow({
flatPermissionFlagMaps: existingMaps,
updatePermissionFlagInput: input,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatToUpdate],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while updating permission flag',
);
}
const { flatPermissionFlagMaps: recomputedMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return fromFlatPermissionFlagToPermissionFlagDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: input.id,
flatEntityMaps: recomputedMaps,
}),
);
}
async delete(id: string, workspaceId: string): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatPermissionFlagMaps: existingMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatToDelete =
fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow({
flatPermissionFlagMaps: existingMaps,
permissionFlagId: id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [],
flatEntityToDelete: [flatToDelete],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while deleting permission flag',
);
}
return fromFlatPermissionFlagToPermissionFlagDto(flatToDelete);
}
}
@@ -1,10 +0,0 @@
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
export const fromFlatPermissionFlagToPermissionFlagDto = (
flatPermissionFlag: FlatPermissionFlag,
): PermissionFlagDTO => ({
id: flatPermissionFlag.id,
roleId: flatPermissionFlag.roleId,
flag: flatPermissionFlag.flag,
});
@@ -1,7 +1,10 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -60,7 +63,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
@@ -133,7 +136,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
@@ -184,7 +187,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -254,7 +257,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -292,8 +295,8 @@ describe('PermissionsService', () => {
});
});
describe('Granular permissions with permissionFlags', () => {
it('should grant specific tool permission when included in permissionFlags even if canAccessAllTools is false', () => {
describe('Granular permissions with rolePermissionFlags', () => {
it('should grant specific tool permission when included in rolePermissionFlags even if canAccessAllTools is false', () => {
const roleWithSpecificPermission: Partial<RoleEntity> = {
id: 'test-role-id',
label: 'Test Role',
@@ -308,10 +311,13 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [
rolePermissionFlags: [
{
id: 'permission-1',
flag: PermissionFlagType.UPLOAD_FILE,
permissionFlag: {
key: PermissionFlagType.UPLOAD_FILE,
universalIdentifier: SystemPermissionFlag.UPLOAD_FILE,
},
roleId: 'test-role-id',
workspaceId: 'test-workspace-id',
createdAt: new Date(),
@@ -338,7 +344,7 @@ describe('PermissionsService', () => {
).toBe(false);
});
it('should grant specific settings permission when included in permissionFlags even if canUpdateAllSettings is false', () => {
it('should grant specific settings permission when included in rolePermissionFlags even if canUpdateAllSettings is false', () => {
const roleWithSpecificPermission: Partial<RoleEntity> = {
id: 'test-role-id',
label: 'Test Role',
@@ -353,10 +359,13 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [
rolePermissionFlags: [
{
id: 'permission-1',
flag: PermissionFlagType.ROLES,
permissionFlag: {
key: PermissionFlagType.ROLES,
universalIdentifier: SystemPermissionFlag.ROLES,
},
roleId: 'test-role-id',
workspaceId: 'test-workspace-id',
createdAt: new Date(),
@@ -400,7 +409,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
@@ -66,7 +69,7 @@ export class PermissionsService {
const defaultSettingsPermissions =
this.getDefaultUserWorkspacePermissions().permissionFlags;
const permissionFlags = Object.keys(PermissionFlagType).reduce(
const permissionFlags = Object.values(PermissionFlagType).reduce(
(acc, feature) => {
const hasBasePermission = this.isToolPermission(feature)
? roleOfUserWorkspace.canAccessAllTools
@@ -76,9 +79,7 @@ export class PermissionsService {
...acc,
[feature]:
hasBasePermission ||
roleOfUserWorkspace.permissionFlags.some(
(permissionFlag) => permissionFlag.flag === feature,
),
this.roleHasPermissionFlag(roleOfUserWorkspace, feature),
};
},
defaultSettingsPermissions,
@@ -150,7 +151,10 @@ export class PermissionsService {
const role = await this.roleRepository.findOne({
where: { id: roleId, workspaceId },
relations: ['permissionFlags'],
relations: [
'rolePermissionFlags',
'rolePermissionFlags.permissionFlag',
],
});
if (!isDefined(role)) {
@@ -203,7 +207,10 @@ export class PermissionsService {
const role = await this.roleRepository.findOne({
where: { id: applicationRoleId, workspaceId },
relations: ['permissionFlags'],
relations: [
'rolePermissionFlags',
'rolePermissionFlags.permissionFlag',
],
});
if (!isDefined(role)) {
@@ -240,10 +247,22 @@ export class PermissionsService {
return true;
}
const permissionFlags = role.permissionFlags ?? [];
return this.roleHasPermissionFlag(role, setting);
}
return permissionFlags.some(
(permissionFlag) => permissionFlag.flag === setting,
private roleHasPermissionFlag(
role: RoleEntity,
flag: PermissionFlagType,
): boolean {
const rolePermissionFlags = role.rolePermissionFlags ?? [];
const permissionFlagUniversalIdentifier = SystemPermissionFlag[flag];
return rolePermissionFlags.some(
(rolePermissionFlag) =>
(rolePermissionFlag.permissionFlag?.universalIdentifier ??
SystemPermissionFlag[rolePermissionFlag.flag]) ===
permissionFlagUniversalIdentifier,
);
}
@@ -292,7 +311,7 @@ export class PermissionsService {
const result = await this.getRolesFromPermissionConfig(
rolePermissionConfig,
workspaceId,
['permissionFlags'],
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
);
if (result === null) {
@@ -318,7 +337,7 @@ export class PermissionsService {
const result = await this.getRolesFromPermissionConfig(
rolePermissionConfig,
workspaceId,
['permissionFlags'],
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
);
if (result === null) {
@@ -332,11 +351,7 @@ export class PermissionsService {
return true;
}
const permissionFlags = role.permissionFlags ?? [];
return permissionFlags.some(
(permissionFlag) => permissionFlag.flag === flag,
);
return this.roleHasPermissionFlag(role, flag);
};
return useIntersection
@@ -0,0 +1,8 @@
import { type PermissionFlagType } from 'twenty-shared/constants';
export type CreateRolePermissionFlagInput = {
roleId: string;
permissionFlagId: string;
flag: PermissionFlagType;
universalIdentifier?: string;
};
@@ -0,0 +1,17 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('RolePermissionFlag')
export class RolePermissionFlagDTO {
@Field(() => UUIDScalarType, { nullable: false })
id: string;
@Field(() => UUIDScalarType, { nullable: false })
roleId: string;
@Field({ nullable: false })
flag: PermissionFlagType;
}
@@ -0,0 +1,61 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('rolePermissionFlag')
@Unique('IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId'])
@Unique('IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID_ROLE_ID_UNIQUE', [
'permissionFlagId',
'roleId',
])
@Index('IDX_ROLE_PERMISSION_FLAG_ROLE_ID', ['roleId'])
@Index('IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID', ['permissionFlagId'])
export class RolePermissionFlagEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.rolePermissionFlags, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
flag: PermissionFlagType;
@Column({ nullable: true, type: 'uuid' })
permissionFlagId: string | null;
@ManyToOne(
() => PermissionFlagEntity,
(permissionFlag) => permissionFlag.rolePermissionFlags,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'permissionFlagId' })
permissionFlag: Relation<PermissionFlagEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RolePermissionFlagService } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.service';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceCacheModule,
ApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [RolePermissionFlagService],
exports: [RolePermissionFlagService],
})
export class RolePermissionFlagModule {}
@@ -0,0 +1,185 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import {
type PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate } from 'src/engine/metadata-modules/flat-role-permission-flag/utils/from-create-role-permission-flag-input-to-flat-role-permission-flag-to-create.util';
import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/upsert-permission-flags.input';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class RolePermissionFlagService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
) {}
public async upsertPermissionFlags({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertPermissionFlagsInput;
}): Promise<FlatRolePermissionFlag[]> {
const { flatPermissionFlagMaps, flatRolePermissionFlagMaps, flatRoleMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatPermissionFlagMaps',
'flatRolePermissionFlagMaps',
'flatRoleMaps',
],
},
);
const roleUniversalId = flatRoleMaps.universalIdentifierById[input.roleId];
const role = isDefined(roleUniversalId)
? flatRoleMaps.byUniversalIdentifier[roleUniversalId]
: undefined;
if (!isDefined(role)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are trying to modify could not be found.`,
},
);
}
const roleUniversalIdentifier = role.universalIdentifier;
const permissionFlagByKey = new Map(
Object.values(flatPermissionFlagMaps.byUniversalIdentifier)
.filter(isDefined)
.map((permissionFlag) => [permissionFlag.key, permissionFlag]),
);
const permissionFlagsByInputKey = input.permissionFlagKeys.map((flag) => ({
flag,
permissionFlag: permissionFlagByKey.get(flag),
}));
const missingPermissionFlags = permissionFlagsByInputKey
.filter(({ permissionFlag }) => !isDefined(permissionFlag))
.map(({ flag }) => flag);
if (missingPermissionFlags.length > 0) {
throw new PermissionsException(
`${PermissionsExceptionMessage.INVALID_SETTING}: ${missingPermissionFlags.join(', ')}`,
PermissionsExceptionCode.INVALID_SETTING,
{
userFriendlyMessage: msg`Some of the permissions you selected are not valid. Please try again with valid permission settings.`,
},
);
}
const currentRolePermissionFlagsForRole = Object.values(
flatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatRolePermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
const inputSet = new Set(
permissionFlagsByInputKey
.map(({ permissionFlag }) => permissionFlag?.universalIdentifier)
.filter(isDefined),
);
const getEffectiveUniversalIdentifier = (
pf: FlatRolePermissionFlag,
): string =>
pf.permissionFlagUniversalIdentifier ?? SystemPermissionFlag[pf.flag];
const existingSet = new Set(
currentRolePermissionFlagsForRole.map(getEffectiveUniversalIdentifier),
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatEntityToCreate = permissionFlagsByInputKey
.map(({ permissionFlag }) => permissionFlag)
.filter(isDefined)
.filter(
(permissionFlag) =>
!existingSet.has(permissionFlag.universalIdentifier),
)
.map((permissionFlag) =>
fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate({
createRolePermissionFlagInput: {
roleId: input.roleId,
permissionFlagId: permissionFlag.id,
flag: permissionFlag.key as PermissionFlagType,
},
flatApplication: workspaceCustomFlatApplication,
flatPermissionFlagMaps,
flatRoleMaps,
}),
);
const flatEntityToDelete = currentRolePermissionFlagsForRole.filter(
(pf) => !inputSet.has(getEffectiveUniversalIdentifier(pf)),
);
if (flatEntityToCreate.length === 0 && flatEntityToDelete.length === 0) {
return currentRolePermissionFlagsForRole.filter((pf) =>
inputSet.has(getEffectiveUniversalIdentifier(pf)),
);
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
rolePermissionFlag: {
flatEntityToCreate,
flatEntityToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting permission flags',
);
}
const { flatRolePermissionFlagMaps: freshFlatRolePermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRolePermissionFlagMaps'],
},
);
const resultFlags = Object.values(
freshFlatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatRolePermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
return resultFlags;
}
}
@@ -0,0 +1,79 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { fromFlatRolePermissionFlagToRolePermissionFlagDto } from 'src/engine/metadata-modules/role-permission-flag/utils/from-flat-role-permission-flag-to-role-permission-flag-dto.util';
const buildFlatRolePermissionFlag = (
overrides: Partial<FlatRolePermissionFlag> = {},
): FlatRolePermissionFlag =>
({
id: 'role-permission-flag-id',
roleId: 'role-id',
workspaceId: 'workspace-id',
applicationId: 'application-id',
applicationUniversalIdentifier: 'application-universal-id',
permissionFlagUniversalIdentifier: 'permission-flag-universal-id',
flag: PermissionFlagType.WORKSPACE,
...overrides,
}) as FlatRolePermissionFlag;
const buildFlatPermissionFlag = (): FlatPermissionFlag => ({
id: 'permission-flag-id',
workspaceId: 'workspace-id',
applicationId: 'application-id',
universalIdentifier: 'permission-flag-universal-id',
applicationUniversalIdentifier: 'application-universal-id',
key: PermissionFlagType.WORKSPACE,
label: 'Workspace',
description: null,
icon: null,
permissionType: 'settings',
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: '2026-05-13T00:00:00.000Z',
updatedAt: '2026-05-13T00:00:00.000Z',
});
const buildFlatPermissionFlagMaps = (): FlatPermissionFlagMaps => ({
byUniversalIdentifier: {
'permission-flag-universal-id': buildFlatPermissionFlag(),
},
universalIdentifierById: {
'permission-flag-id': 'permission-flag-universal-id',
},
universalIdentifiersByApplicationId: {},
});
describe('fromFlatRolePermissionFlagToRolePermissionFlagDto', () => {
it('maps role permission flags to the public permission flag DTO shape', () => {
expect(
fromFlatRolePermissionFlagToRolePermissionFlagDto(
buildFlatRolePermissionFlag(),
buildFlatPermissionFlagMaps(),
),
).toEqual({
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
});
});
it('falls back to the legacy flag column when the catalog is missing the entry', () => {
expect(
fromFlatRolePermissionFlagToRolePermissionFlagDto(
buildFlatRolePermissionFlag(),
{
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
),
).toEqual({
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
});
});
});
@@ -0,0 +1,36 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { synthesizeFlatPermissionFlagFromFlag } from 'src/engine/metadata-modules/flat-permission-flag/utils/synthesize-flat-permission-flag-from-flag.util';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
export const fromFlatRolePermissionFlagToRolePermissionFlagDto = (
flatRolePermissionFlag: FlatRolePermissionFlag,
flatPermissionFlagMaps: FlatPermissionFlagMaps,
): RolePermissionFlagDTO => {
const catalogPermissionFlag = isDefined(
flatRolePermissionFlag.permissionFlagUniversalIdentifier,
)
? flatPermissionFlagMaps.byUniversalIdentifier[
flatRolePermissionFlag.permissionFlagUniversalIdentifier
]
: undefined;
const permissionFlag = isDefined(catalogPermissionFlag)
? catalogPermissionFlag
: synthesizeFlatPermissionFlagFromFlag({
flag: flatRolePermissionFlag.flag,
workspaceId: flatRolePermissionFlag.workspaceId,
applicationId: flatRolePermissionFlag.applicationId,
applicationUniversalIdentifier:
flatRolePermissionFlag.applicationUniversalIdentifier,
});
return {
id: flatRolePermissionFlag.id,
roleId: flatRolePermissionFlag.roleId,
flag: permissionFlag.key as PermissionFlagType,
};
};
@@ -7,7 +7,7 @@ import { WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
import { type RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto';
import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto';
@@ -86,8 +86,8 @@ export class RoleDTO {
@Field({ nullable: false })
canDestroyAllObjectRecords: boolean;
@Field(() => [PermissionFlagDTO], { nullable: true })
permissionFlags?: PermissionFlagDTO[];
@Field(() => [RolePermissionFlagDTO], { nullable: true })
permissionFlags?: RolePermissionFlagDTO[];
@Field(() => [ObjectPermissionDTO], { nullable: true })
objectPermissions?: ObjectPermissionDTO[];
@@ -11,7 +11,7 @@ import {
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -81,10 +81,10 @@ export class RoleEntity extends SyncableEntity implements Required<RoleEntity> {
objectPermissions: Relation<ObjectPermissionEntity[]>;
@OneToMany(
() => PermissionFlagEntity,
(permissionFlag: PermissionFlagEntity) => permissionFlag.role,
() => RolePermissionFlagEntity,
(rolePermissionFlag: RolePermissionFlagEntity) => rolePermissionFlag.role,
)
permissionFlags: Relation<PermissionFlagEntity[]>;
rolePermissionFlags: Relation<RolePermissionFlagEntity[]>;
@OneToMany(
() => FieldPermissionEntity,
@@ -15,8 +15,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RolePermissionFlagModule } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -39,7 +39,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
RoleEntity,
RoleTargetEntity,
ObjectPermissionEntity,
PermissionFlagEntity,
RolePermissionFlagEntity,
FieldPermissionEntity,
UserWorkspaceEntity,
ObjectMetadataEntity,
@@ -52,7 +52,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
ApiKeyModule,
PermissionsModule,
ObjectPermissionModule,
PermissionFlagModule,
RolePermissionFlagModule,
RowLevelPermissionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
@@ -33,16 +33,17 @@ import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto
import { fromFlatAgentWithRoleIdToAgentDto } from 'src/engine/metadata-modules/flat-agent/utils/from-agent-entity-to-agent-dto.util';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { fromFlatFieldPermissionToFieldPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-field-permission-to-field-permission-dto.util';
import { fromFlatObjectPermissionToObjectPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
import { RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/upsert-permission-flags.input';
import { RolePermissionFlagService } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.service';
import { fromFlatRolePermissionFlagToRolePermissionFlagDto } from 'src/engine/metadata-modules/role-permission-flag/utils/from-flat-role-permission-flag-to-role-permission-flag-dto.util';
import {
PermissionsException,
PermissionsExceptionCode,
@@ -87,7 +88,7 @@ export class RoleResolver {
private readonly roleService: RoleService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly objectPermissionService: ObjectPermissionService,
private readonly settingPermissionService: PermissionFlagService,
private readonly rolePermissionFlagService: RolePermissionFlagService,
private readonly agentRoleService: AiAgentRoleService,
private readonly apiKeyRoleService: ApiKeyRoleService,
private readonly fieldPermissionService: FieldPermissionService,
@@ -95,6 +96,7 @@ export class RoleResolver {
private readonly rowLevelPermissionPredicateService: RowLevelPermissionPredicateService,
private readonly rowLevelPermissionPredicateGroupService: RowLevelPermissionPredicateGroupService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@Query(() => [RoleDTO])
@@ -223,18 +225,31 @@ export class RoleResolver {
);
}
@Mutation(() => [PermissionFlagDTO])
@Mutation(() => [RolePermissionFlagDTO])
async upsertPermissionFlags(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('upsertPermissionFlagsInput')
upsertPermissionFlagsInput: UpsertPermissionFlagsInput,
): Promise<PermissionFlagDTO[]> {
const flatPermissionFlags =
await this.settingPermissionService.upsertPermissionFlags({
): Promise<RolePermissionFlagDTO[]> {
const flatRolePermissionFlags =
await this.rolePermissionFlagService.upsertPermissionFlags({
workspaceId: workspace.id,
input: upsertPermissionFlagsInput,
});
return flatPermissionFlags.map(fromFlatPermissionFlagToPermissionFlagDto);
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: workspace.id,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return flatRolePermissionFlags.map((flatRolePermissionFlag) =>
fromFlatRolePermissionFlagToRolePermissionFlagDto(
flatRolePermissionFlag,
flatPermissionFlagMaps,
),
);
}
@Mutation(() => [FieldPermissionDTO])
@@ -49,7 +49,9 @@ export class RoleService {
},
relations: {
roleTargets: true,
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
objectPermissions: true,
fieldPermissions: true,
},
@@ -67,7 +69,9 @@ export class RoleService {
},
relations: {
roleTargets: true,
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
objectPermissions: true,
fieldPermissions: true,
},
@@ -1,14 +1,17 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -23,7 +26,7 @@ const PERSON_OBJECT_METADATA_ID = '44444444-4444-4444-8444-444444444444';
const createBaseRole = (
overrides: Partial<RoleEntity> &
Pick<RoleEntity, 'permissionFlags' | 'objectPermissions'>,
Pick<RoleEntity, 'rolePermissionFlags' | 'objectPermissions'>,
): RoleEntity =>
({
id: ROLE_ID,
@@ -56,8 +59,8 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
let objectPermissionRepository: jest.Mocked<
Pick<Repository<ObjectPermissionEntity>, 'find'>
>;
let permissionFlagRepository: jest.Mocked<
Pick<Repository<PermissionFlagEntity>, 'find'>
let rolePermissionFlagRepository: jest.Mocked<
Pick<Repository<RolePermissionFlagEntity>, 'find'>
>;
const workspaceObjectMetadataFixture: ObjectMetadataEntity[] = [
@@ -94,7 +97,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
find: jest.fn().mockResolvedValue([]),
};
permissionFlagRepository = {
rolePermissionFlagRepository = {
find: jest.fn().mockResolvedValue([]),
};
const fieldPermissionRepository = {
@@ -123,8 +126,8 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
useValue: objectPermissionRepository,
},
{
provide: getRepositoryToken(PermissionFlagEntity),
useValue: permissionFlagRepository,
provide: getRepositoryToken(RolePermissionFlagEntity),
useValue: rolePermissionFlagRepository,
},
{
provide: getRepositoryToken(FieldPermissionEntity),
@@ -148,7 +151,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
it('should deny all record permissions when role has neither workspace members access nor update-all-settings', async () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -164,16 +167,19 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
});
it('should grant all record permissions when role has WORKSPACE_MEMBERS permission flag', async () => {
permissionFlagRepository.find.mockResolvedValue([
rolePermissionFlagRepository.find.mockResolvedValue([
{
roleId: ROLE_ID,
flag: PermissionFlagType.WORKSPACE_MEMBERS,
} as PermissionFlagEntity,
permissionFlag: {
key: PermissionFlagType.WORKSPACE_MEMBERS,
universalIdentifier: SystemPermissionFlag.WORKSPACE_MEMBERS,
},
} as RolePermissionFlagEntity,
]);
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -192,7 +198,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
canUpdateAllSettings: true,
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -210,7 +216,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
it('should deny all record permissions when role has neither workflows access nor update-all-settings', async () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -225,16 +231,19 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
});
it('should grant all record permissions when role has WORKFLOWS permission flag', async () => {
permissionFlagRepository.find.mockResolvedValue([
rolePermissionFlagRepository.find.mockResolvedValue([
{
roleId: ROLE_ID,
flag: PermissionFlagType.WORKFLOWS,
} as PermissionFlagEntity,
permissionFlag: {
key: PermissionFlagType.WORKFLOWS,
universalIdentifier: SystemPermissionFlag.WORKFLOWS,
},
} as RolePermissionFlagEntity,
]);
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -264,7 +273,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -285,7 +294,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -12,7 +12,7 @@ import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-
import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -36,8 +36,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
@InjectRepository(RowLevelPermissionPredicateEntity)
@@ -56,7 +56,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
applications,
roleTargets,
objectPermissions,
permissionFlags,
rolePermissionFlags,
fieldPermissions,
rowLevelPermissionPredicates,
rowLevelPermissionPredicateGroups,
@@ -80,7 +80,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
this.permissionFlagRepository.find({
this.rolePermissionFlagRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
@@ -105,7 +105,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
const [
roleTargetsByRoleId,
objectPermissionsByRoleId,
permissionFlagsByRoleId,
rolePermissionFlagsByRoleId,
fieldPermissionsByRoleId,
rowLevelPermissionPredicatesByRoleId,
rowLevelPermissionPredicateGroupsByRoleId,
@@ -120,7 +120,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
foreignKey: 'roleId',
},
{
entities: permissionFlags,
entities: rolePermissionFlags,
foreignKey: 'roleId',
},
{
@@ -149,7 +149,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
...roleEntity,
roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [],
objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [],
permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [],
rolePermissionFlags:
rolePermissionFlagsByRoleId.get(roleEntity.id) || [],
fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [],
rowLevelPermissionPredicates:
rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [],
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
type ObjectsPermissions,
@@ -16,7 +19,7 @@ import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/wo
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -41,8 +44,8 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
@InjectRepository(RowLevelPermissionPredicateEntity)
@@ -59,7 +62,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const [
roles,
objectPermissions,
permissionFlags,
rolePermissionFlags,
fieldPermissions,
rowLevelPermissionPredicates,
rowLevelPermissionPredicateGroups,
@@ -71,8 +74,9 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
this.objectPermissionRepository.find({
where: { workspaceId },
}),
this.permissionFlagRepository.find({
this.rolePermissionFlagRepository.find({
where: { workspaceId },
relations: ['permissionFlag'],
}),
this.fieldPermissionRepository.find({
where: { workspaceId },
@@ -91,9 +95,9 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
entities: objectPermissions,
foreignKey: 'roleId',
});
const permissionFlagsByRoleId =
regroupEntitiesByRelatedEntityId<'permissionFlag'>({
entities: permissionFlags,
const rolePermissionFlagsByRoleId =
regroupEntitiesByRelatedEntityId<'rolePermissionFlag'>({
entities: rolePermissionFlags,
foreignKey: 'roleId',
});
const fieldPermissionsByRoleId =
@@ -117,7 +121,8 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
for (const role of roles) {
const roleObjectPermissions =
objectPermissionsByRoleId.get(role.id) ?? [];
const rolePermissionFlags = permissionFlagsByRoleId.get(role.id) ?? [];
const roleRolePermissionFlags =
rolePermissionFlagsByRoleId.get(role.id) ?? [];
const roleFieldPermissions = fieldPermissionsByRoleId.get(role.id) ?? [];
const roleRowLevelPermissionPredicates =
rowLevelPermissionPredicatesByRoleId.get(role.id) ?? [];
@@ -150,7 +155,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const hasWorkflowsPermissions =
this.hasSettingsGatedObjectPermissions(
role,
rolePermissionFlags,
roleRolePermissionFlags,
PermissionFlagType.WORKFLOWS,
);
@@ -163,7 +168,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const hasWorkspaceMembersPermissions =
this.hasSettingsGatedObjectPermissions(
role,
rolePermissionFlags,
roleRolePermissionFlags,
PermissionFlagType.WORKSPACE_MEMBERS,
);
@@ -269,13 +274,18 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
private hasSettingsGatedObjectPermissions(
role: RoleEntity,
permissionFlags: PermissionFlagEntity[],
rolePermissionFlags: RolePermissionFlagEntity[],
permissionFlagType: PermissionFlagType,
): boolean {
const hasPermissionFromRole = role.canUpdateAllSettings;
const permissionFlagUniversalIdentifier =
SystemPermissionFlag[permissionFlagType];
const hasPermissionFromSettingPermissions = isDefined(
permissionFlags.find(
(permissionFlag) => permissionFlag.flag === permissionFlagType,
rolePermissionFlags.find(
(rolePermissionFlag) =>
(rolePermissionFlag.permissionFlag?.universalIdentifier ??
SystemPermissionFlag[rolePermissionFlag.flag]) ===
permissionFlagUniversalIdentifier,
),
);
@@ -0,0 +1,65 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
const buildRoleEntity = (overrides: Partial<RoleEntity> = {}): RoleEntity =>
({
id: 'role-id',
label: 'Role',
universalIdentifier: 'role-universal-identifier',
canUpdateAllSettings: false,
canAccessAllTools: false,
description: null,
icon: null,
isEditable: true,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
rolePermissionFlags: [],
...overrides,
}) as RoleEntity;
describe('fromRoleEntityToRoleDto', () => {
it('returns undefined permission flags when role permission flags are not loaded', () => {
const roleDto = fromRoleEntityToRoleDto(
buildRoleEntity({
rolePermissionFlags:
undefined as unknown as RoleEntity['rolePermissionFlags'],
}),
);
expect(roleDto.permissionFlags).toBeUndefined();
});
it('maps loaded role permission flags to the public permission flag DTO shape', () => {
const roleDto = fromRoleEntityToRoleDto(
buildRoleEntity({
rolePermissionFlags: [
{
id: 'role-permission-flag-id',
roleId: 'role-id',
permissionFlag: {
key: PermissionFlagType.WORKSPACE,
},
},
] as RoleEntity['rolePermissionFlags'],
}),
);
expect(roleDto.permissionFlags).toEqual([
{
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
},
]);
});
});
@@ -1,3 +1,5 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -19,7 +21,12 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
canBeAssignedToAgents: role.canBeAssignedToAgents,
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
roleTargets: role.roleTargets,
permissionFlags: role.permissionFlags,
permissionFlags: role.rolePermissionFlags?.map((rolePermissionFlag) => ({
id: rolePermissionFlag.id,
roleId: rolePermissionFlag.roleId,
flag: (rolePermissionFlag.permissionFlag?.key ??
rolePermissionFlag.flag) as PermissionFlagType,
})),
objectPermissions: role.objectPermissions,
fieldPermissions: role.fieldPermissions,
};
@@ -108,7 +108,9 @@ export class UserRoleService {
},
relations: {
role: {
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
},
},
});