[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:
+13
-2
@@ -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",
|
||||
|
||||
+45
-3
@@ -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,
|
||||
|
||||
+8
-1
@@ -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,
|
||||
|
||||
+13
-2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
|
||||
logicFunction: {},
|
||||
role: {},
|
||||
roleTarget: {},
|
||||
rolePermissionFlag: {},
|
||||
permissionFlag: {},
|
||||
objectPermission: {},
|
||||
fieldPermission: {},
|
||||
|
||||
+13
-5
@@ -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: {
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
+27
@@ -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: {
|
||||
|
||||
+9
-2
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+4
-2
@@ -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'>[];
|
||||
|
||||
+22
@@ -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 {}
|
||||
+34
-27
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+25
-19
@@ -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,
|
||||
};
|
||||
|
||||
+31
@@ -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;
|
||||
};
|
||||
+18
@@ -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),
|
||||
});
|
||||
+10
-17
@@ -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,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
+42
@@ -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(),
|
||||
};
|
||||
};
|
||||
+57
@@ -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,
|
||||
};
|
||||
};
|
||||
+86
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
@@ -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>;
|
||||
+4
@@ -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>;
|
||||
+45
@@ -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,
|
||||
};
|
||||
};
|
||||
+71
@@ -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
-2
@@ -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[];
|
||||
};
|
||||
|
||||
+2
-2
@@ -54,9 +54,9 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
|
||||
roleTargetIds: [],
|
||||
roleTargetUniversalIdentifiers: [],
|
||||
objectPermissionIds: [],
|
||||
permissionFlagIds: [],
|
||||
rolePermissionFlagIds: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
rolePermissionFlagUniversalIdentifiers: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateIds: [],
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const PERMISSION_FLAG_PERMISSION_TYPES = ['settings', 'tool'] as const;
|
||||
|
||||
export type PermissionFlagPermissionType =
|
||||
(typeof PERMISSION_FLAG_PERMISSION_TYPES)[number];
|
||||
+160
@@ -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',
|
||||
}));
|
||||
+50
-5
@@ -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;
|
||||
}
|
||||
|
||||
+57
-7
@@ -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;
|
||||
}
|
||||
|
||||
+57
@@ -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;
|
||||
}
|
||||
+23
-15
@@ -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;
|
||||
|
||||
+52
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -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],
|
||||
|
||||
+200
-125
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
-10
@@ -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,
|
||||
});
|
||||
+22
-13
@@ -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: [],
|
||||
|
||||
+32
-17
@@ -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
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
export type CreateRolePermissionFlagInput = {
|
||||
roleId: string;
|
||||
permissionFlagId: string;
|
||||
flag: PermissionFlagType;
|
||||
universalIdentifier?: string;
|
||||
};
|
||||
+17
@@ -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;
|
||||
}
|
||||
+61
@@ -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;
|
||||
}
|
||||
+19
@@ -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 {}
|
||||
+185
@@ -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;
|
||||
}
|
||||
}
|
||||
+79
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+36
@@ -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,
|
||||
},
|
||||
|
||||
+30
-21
@@ -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: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
+9
-8
@@ -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) || [],
|
||||
|
||||
+25
-15
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
+65
@@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+8
-1
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user