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

Split of #20377.

## Summary

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

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

## What changed

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

## Behavior after this PR

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

## What is not in this PR

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

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
nitin
2026-05-18 17:27:47 +05:30
committed by GitHub
parent 01535a3b3e
commit db0547f503
132 changed files with 3663 additions and 719 deletions
@@ -244,7 +244,7 @@ type FieldPermission {
canUpdateFieldValue: Boolean
}
type PermissionFlag {
type RolePermissionFlag {
id: UUID!
roleId: UUID!
flag: PermissionFlagType!
@@ -276,7 +276,7 @@ type Role {
canUpdateAllObjectRecords: Boolean!
canSoftDeleteAllObjectRecords: Boolean!
canDestroyAllObjectRecords: Boolean!
permissionFlags: [PermissionFlag!]
permissionFlags: [RolePermissionFlag!]
objectPermissions: [ObjectPermission!]
fieldPermissions: [FieldPermission!]
rowLevelPermissionPredicates: [RowLevelPermissionPredicate!]
@@ -2884,6 +2884,7 @@ enum AllMetadataName {
pageLayoutTab
commandMenuItem
navigationMenuItem
rolePermissionFlag
permissionFlag
objectPermission
fieldPermission
@@ -3237,7 +3238,7 @@ type Mutation {
updateOneRole(updateRoleInput: UpdateRoleInput!): Role!
deleteOneRole(roleId: UUID!): String!
upsertObjectPermissions(upsertObjectPermissionsInput: UpsertObjectPermissionsInput!): [ObjectPermission!]!
upsertPermissionFlags(upsertPermissionFlagsInput: UpsertPermissionFlagsInput!): [PermissionFlag!]!
upsertPermissionFlags(upsertPermissionFlagsInput: UpsertPermissionFlagsInput!): [RolePermissionFlag!]!
upsertFieldPermissions(upsertFieldPermissionsInput: UpsertFieldPermissionsInput!): [FieldPermission!]!
upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult!
assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean!
@@ -190,11 +190,11 @@ export interface FieldPermission {
__typename: 'FieldPermission'
}
export interface PermissionFlag {
export interface RolePermissionFlag {
id: Scalars['UUID']
roleId: Scalars['UUID']
flag: PermissionFlagType
__typename: 'PermissionFlag'
__typename: 'RolePermissionFlag'
}
export interface ApiKeyForRole {
@@ -224,7 +224,7 @@ export interface Role {
canUpdateAllObjectRecords: Scalars['Boolean']
canSoftDeleteAllObjectRecords: Scalars['Boolean']
canDestroyAllObjectRecords: Scalars['Boolean']
permissionFlags?: PermissionFlag[]
permissionFlags?: RolePermissionFlag[]
objectPermissions?: ObjectPermission[]
fieldPermissions?: FieldPermission[]
rowLevelPermissionPredicates?: RowLevelPermissionPredicate[]
@@ -2516,7 +2516,7 @@ export interface CollectionHash {
__typename: 'CollectionHash'
}
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'rolePermissionFlag' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
export interface MinimalObjectMetadata {
id: Scalars['UUID']
@@ -2772,7 +2772,7 @@ export interface Mutation {
updateOneRole: Role
deleteOneRole: Scalars['String']
upsertObjectPermissions: ObjectPermission[]
upsertPermissionFlags: PermissionFlag[]
upsertPermissionFlags: RolePermissionFlag[]
upsertFieldPermissions: FieldPermission[]
upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult
assignRoleToAgent: Scalars['Boolean']
@@ -3072,7 +3072,7 @@ export interface FieldPermissionGenqlSelection{
__scalar?: boolean | number
}
export interface PermissionFlagGenqlSelection{
export interface RolePermissionFlagGenqlSelection{
id?: boolean | number
roleId?: boolean | number
flag?: boolean | number
@@ -3108,7 +3108,7 @@ export interface RoleGenqlSelection{
canUpdateAllObjectRecords?: boolean | number
canSoftDeleteAllObjectRecords?: boolean | number
canDestroyAllObjectRecords?: boolean | number
permissionFlags?: PermissionFlagGenqlSelection
permissionFlags?: RolePermissionFlagGenqlSelection
objectPermissions?: ObjectPermissionGenqlSelection
fieldPermissions?: FieldPermissionGenqlSelection
rowLevelPermissionPredicates?: RowLevelPermissionPredicateGenqlSelection
@@ -5847,7 +5847,7 @@ export interface MutationGenqlSelection{
updateOneRole?: (RoleGenqlSelection & { __args: {updateRoleInput: UpdateRoleInput} })
deleteOneRole?: { __args: {roleId: Scalars['UUID']} }
upsertObjectPermissions?: (ObjectPermissionGenqlSelection & { __args: {upsertObjectPermissionsInput: UpsertObjectPermissionsInput} })
upsertPermissionFlags?: (PermissionFlagGenqlSelection & { __args: {upsertPermissionFlagsInput: UpsertPermissionFlagsInput} })
upsertPermissionFlags?: (RolePermissionFlagGenqlSelection & { __args: {upsertPermissionFlagsInput: UpsertPermissionFlagsInput} })
upsertFieldPermissions?: (FieldPermissionGenqlSelection & { __args: {upsertFieldPermissionsInput: UpsertFieldPermissionsInput} })
upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} })
assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} }
@@ -6426,10 +6426,10 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const PermissionFlag_possibleTypes: string[] = ['PermissionFlag']
export const isPermissionFlag = (obj?: { __typename?: any } | null): obj is PermissionFlag => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPermissionFlag"')
return PermissionFlag_possibleTypes.includes(obj.__typename)
const RolePermissionFlag_possibleTypes: string[] = ['RolePermissionFlag']
export const isRolePermissionFlag = (obj?: { __typename?: any } | null): obj is RolePermissionFlag => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRolePermissionFlag"')
return RolePermissionFlag_possibleTypes.includes(obj.__typename)
}
@@ -8899,6 +8899,7 @@ export const enumAllMetadataName = {
pageLayoutTab: 'pageLayoutTab' as const,
commandMenuItem: 'commandMenuItem' as const,
navigationMenuItem: 'navigationMenuItem' as const,
rolePermissionFlag: 'rolePermissionFlag' as const,
permissionFlag: 'permissionFlag' as const,
objectPermission: 'objectPermission' as const,
fieldPermission: 'fieldPermission' as const,
@@ -500,7 +500,7 @@ export default {
1
]
},
"PermissionFlag": {
"RolePermissionFlag": {
"id": [
3
],
@@ -1,7 +1,7 @@
---
title: Roles & Permissions
description: Declare what objects and fields your app's logic functions and front components can read and write.
icon: "shield-halved"
icon: 'shield-halved'
---
A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role marked with `defineApplicationRole()` (see [The default function role](#the-default-function-role) below).
@@ -40,7 +40,8 @@ export default defineRole({
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@@ -80,6 +81,7 @@ export default defineApplicationRole({
`defineApplicationRole()` is a thin wrapper around `defineRole()` that flags **the** role used as your application's default at install time. Validation is identical to `defineRole`, but the build pipeline auto-wires its `universalIdentifier` into the application manifest's `defaultRoleUniversalIdentifier` — so you do not need to reference it from [`defineApplication`](/developers/extend/apps/config/application) yourself.
Notes:
- Exactly **one** `defineApplicationRole(...)` is allowed per app — the manifest build will fail if it finds more than one.
- Use `defineRole()` (not `defineApplicationRole()`) for any **additional** roles your app ships.
- Setting `defaultRoleUniversalIdentifier` explicitly on `defineApplication()` is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
File diff suppressed because one or more lines are too long
@@ -30,6 +30,7 @@ export const useMetadataErrorHandler = () => {
viewFilter: t`view filter`,
index: t`index`,
logicFunction: t`logic function`,
rolePermissionFlag: t`role permission flag`,
permissionFlag: t`permission flag`,
objectPermission: t`object permission`,
fieldPermission: t`field permission`,
@@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const PERMISSION_FLAG_FRAGMENT = gql`
fragment PermissionFlagFragment on PermissionFlag {
id
flag
roleId
}
`;
@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const ROLE_PERMISSION_FLAG_FRAGMENT = gql`
fragment RolePermissionFlagFragment on RolePermissionFlag {
id
flag
roleId
}
`;
@@ -1,15 +1,15 @@
import { PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/permissionFlagFragment';
import { ROLE_PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/rolePermissionFlagFragment';
import { gql } from '@apollo/client';
export const UPSERT_PERMISSION_FLAGS = gql`
${PERMISSION_FLAG_FRAGMENT}
${ROLE_PERMISSION_FLAG_FRAGMENT}
mutation UpsertPermissionFlags(
$upsertPermissionFlagsInput: UpsertPermissionFlagsInput!
) {
upsertPermissionFlags(
upsertPermissionFlagsInput: $upsertPermissionFlagsInput
) {
...PermissionFlagFragment
...RolePermissionFlagFragment
}
}
`;
@@ -2,7 +2,7 @@ import { AGENT_FRAGMENT } from '@/ai/graphql/fragments/agentFragment';
import { API_KEY_FOR_ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/apiKeyForRoleFragment';
import { FIELD_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/fieldPermissionFragment';
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/permissionFlagFragment';
import { ROLE_PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/rolePermissionFlagFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateFragment';
import { ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateGroupFragment';
@@ -14,7 +14,7 @@ export const GET_ROLES = gql`
${ROLE_FRAGMENT}
${AGENT_FRAGMENT}
${API_KEY_FOR_ROLE_FRAGMENT}
${PERMISSION_FLAG_FRAGMENT}
${ROLE_PERMISSION_FLAG_FRAGMENT}
${OBJECT_PERMISSION_FRAGMENT}
${FIELD_PERMISSION_FRAGMENT}
${ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT}
@@ -32,7 +32,7 @@ export const GET_ROLES = gql`
...ApiKeyForRoleFragment
}
permissionFlags {
...PermissionFlagFragment
...RolePermissionFlagFragment
}
objectPermissions {
...ObjectPermissionFragment
@@ -6,16 +6,18 @@ import {
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useHasPermissionFlag = (permissionFlag?: PermissionFlagType) => {
export const useHasPermissionFlag = (
permissionFlagKey?: PermissionFlagType,
) => {
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const currentUserWorkspace = useAtomStateValue(currentUserWorkspaceState);
if (!permissionFlag) {
if (!permissionFlagKey) {
return true;
}
if (
permissionFlag === PermissionFlagType.WORKSPACE &&
permissionFlagKey === PermissionFlagType.WORKSPACE &&
currentWorkspace?.activationStatus ===
WorkspaceActivationStatus.PENDING_CREATION
) {
@@ -23,5 +25,5 @@ export const useHasPermissionFlag = (permissionFlag?: PermissionFlagType) => {
}
const userFlags = currentUserWorkspace?.permissionFlags ?? [];
return userFlags.includes(permissionFlag);
return userFlags.includes(permissionFlagKey);
};
@@ -92,11 +92,6 @@ export const SettingsApplicationDetails = () => {
const displayName =
app?.displayName ?? application?.name ?? t`Application details`;
const description = app?.description ?? resolvedDescription;
const logoUrl =
app?.logoUrl ??
application?.logo ??
application?.applicationRegistration?.logoUrl ??
undefined;
const getScreenshots = () => {
if (app?.screenshots?.length) return app.screenshots;
@@ -123,7 +123,7 @@ const buildSyntheticRole = (
})),
permissionFlags: (defaultRole.permissionFlags ?? []).map(
(permissionFlag) => ({
__typename: 'PermissionFlag' as const,
__typename: 'RolePermissionFlag' as const,
id: uuidv4(),
roleId: defaultRole.universalIdentifier,
flag: permissionFlag.flag,
@@ -32,6 +32,7 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
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 { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/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';
@@ -66,7 +67,7 @@ const WORKSPACE_RELATED_ENTITIES: EntityTarget<ObjectLiteral>[] = [
ViewFilterGroupEntity,
FieldPermissionEntity,
ObjectPermissionEntity,
PermissionFlagEntity,
RolePermissionFlagEntity,
RoleTargetEntity,
SearchFieldMetadataEntity,
RowLevelPermissionPredicateEntity,
@@ -81,6 +82,7 @@ const WORKSPACE_RELATED_ENTITIES: EntityTarget<ObjectLiteral>[] = [
// Level 2: Children that depend on core entities
FieldMetadataEntity,
PageLayoutEntity,
PermissionFlagEntity,
SkillEntity,
LogicFunctionEntity,
@@ -0,0 +1,102 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.6.0', 1778235340020)
export class RenamePermissionFlagToRolePermissionFlagFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
// The legacy permissionFlag table stores per-role grants (`roleId` + `flag`).
// Rename it to preserve those rows before creating the new permissionFlag catalog table.
await queryRunner.query(
`ALTER TABLE "core"."permissionFlag" RENAME TO "rolePermissionFlag"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "PK_a02789db60620a1e9f90147b50f" TO "PK_76591adc8035c2e7b0cd6115136"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE" TO "IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE"`,
);
await queryRunner.query(
`ALTER INDEX "core"."IDX_PERMISSION_FLAG_ROLE_ID" RENAME TO "IDX_ROLE_PERMISSION_FLAG_ROLE_ID"`,
);
// Re-hash inherited constraints/indexes so TypeORM's schema diff matches
// the renamed table. Original names were derived from "permissionFlag"
// and stay free for the new catalog table created by the next migration.
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_13f8ca9c517976733a1ce4c10eb"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_835bc9f7ef959debfc5cd268049"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_da8ffd3c24b4a819430a861067"`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_e4559ae0dba56e53714137c704" ON "core"."rolePermissionFlag" ("workspaceId", "universalIdentifier")`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_d47b1ebee75d98daa0c870c26e3" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_3835ecc1019327566d35728c8ba" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_4c6ea38698de230b0ec18fa2110" FOREIGN KEY ("roleId") REFERENCES "core"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_4c6ea38698de230b0ec18fa2110"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_3835ecc1019327566d35728c8ba"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_d47b1ebee75d98daa0c870c26e3"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_e4559ae0dba56e53714137c704"`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_da8ffd3c24b4a819430a861067" ON "core"."rolePermissionFlag" ("workspaceId", "universalIdentifier")`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_835bc9f7ef959debfc5cd268049" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_13f8ca9c517976733a1ce4c10eb" FOREIGN KEY ("roleId") REFERENCES "core"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER INDEX "core"."IDX_ROLE_PERMISSION_FLAG_ROLE_ID" RENAME TO "IDX_PERMISSION_FLAG_ROLE_ID"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE" TO "IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "PK_76591adc8035c2e7b0cd6115136" TO "PK_a02789db60620a1e9f90147b50f"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag" RENAME TO "permissionFlag"`,
);
}
}
@@ -0,0 +1,69 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.6.0', 1778235340021)
export class PermissionFlagSyncableEntityFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "core"."permissionFlag" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"workspaceId" uuid NOT NULL,
"applicationId" uuid NOT NULL,
"universalIdentifier" uuid NOT NULL,
"key" varchar NOT NULL,
"label" varchar NOT NULL,
"description" text,
"icon" varchar,
"permissionType" varchar NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "IDX_PERMISSION_FLAG_KEY_WORKSPACE_ID_UNIQUE" UNIQUE ("key", "workspaceId"),
CONSTRAINT "PK_a02789db60620a1e9f90147b50f" PRIMARY KEY ("id")
)`,
);
await queryRunner.query(
`CREATE INDEX "IDX_PERMISSION_FLAG_APPLICATION_ID" ON "core"."permissionFlag" ("applicationId")`,
);
// Reuses canonical hash names freed by the rename migration so TypeORM's
// schema diff matches without further intervention.
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_da8ffd3c24b4a819430a861067" ON "core"."permissionFlag" ("workspaceId", "universalIdentifier")`,
);
await queryRunner.query(
`ALTER TABLE "core"."permissionFlag"
ADD CONSTRAINT "FK_835bc9f7ef959debfc5cd268049"
FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id")
ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."permissionFlag"
ADD CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5"
FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id")
ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT IF EXISTS "FK_b26a9d39a88d0e72373c677c6c5"`,
);
await queryRunner.query(
`ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT IF EXISTS "FK_835bc9f7ef959debfc5cd268049"`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_da8ffd3c24b4a819430a861067"`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_PERMISSION_FLAG_APPLICATION_ID"`,
);
await queryRunner.query(`DROP TABLE IF EXISTS "core"."permissionFlag"`);
}
}
@@ -0,0 +1,55 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.6.0', 1778235340022)
export class LinkRolePermissionFlagToPermissionFlagFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag"
ADD COLUMN IF NOT EXISTS "permissionFlagId" uuid`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag"
ADD CONSTRAINT "IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID_ROLE_ID_UNIQUE"
UNIQUE ("permissionFlagId", "roleId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID"
ON "core"."rolePermissionFlag" ("permissionFlagId")`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag"
ADD CONSTRAINT "FK_8724e63323f1331591a3e91b0b3"
FOREIGN KEY ("permissionFlagId") REFERENCES "core"."permissionFlag"("id")
ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag"
DROP CONSTRAINT IF EXISTS "FK_8724e63323f1331591a3e91b0b3"`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag"
DROP CONSTRAINT IF EXISTS "IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID_ROLE_ID_UNIQUE"`,
);
await queryRunner.query(
`ALTER TABLE "core"."rolePermissionFlag"
DROP COLUMN IF EXISTS "permissionFlagId"`,
);
}
}
@@ -0,0 +1,88 @@
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
import { PermissionFlagType } from 'twenty-shared/constants';
import { DataSource, QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
import { STANDARD_PERMISSION_FLAG_DEFINITIONS } from 'src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant';
const PERMISSION_FLAG_TYPES = Object.values(PermissionFlagType) as string[];
@RegisteredInstanceCommand('2.6.0', 1778235340023, { type: 'slow' })
export class BackfillRolePermissionFlagPermissionFlagIdSlowInstanceCommand
implements SlowInstanceCommand
{
async runDataMigration(dataSource: DataSource): Promise<void> {
const unknownFlagRows: { flag: string }[] = await dataSource.query(
`SELECT DISTINCT "flag" FROM "core"."rolePermissionFlag"
WHERE "flag" <> ALL($1::varchar[])`,
[PERMISSION_FLAG_TYPES],
);
if (unknownFlagRows.length > 0) {
const unknownFlags = unknownFlagRows.map((row) => row.flag).join(', ');
throw new Error(
`Cannot migrate: rolePermissionFlag rows reference unknown flag value(s): ${unknownFlags}`,
);
}
for (const definition of STANDARD_PERMISSION_FLAG_DEFINITIONS) {
await dataSource.query(
`INSERT INTO "core"."permissionFlag" (
"id",
"workspaceId",
"applicationId",
"universalIdentifier",
"key",
"label",
"description",
"icon",
"permissionType",
"createdAt",
"updatedAt"
)
SELECT
uuid_generate_v4(),
workspace."id",
standardApplication."id",
$1::uuid,
$2,
$3,
$4,
$5,
$6,
now(),
now()
FROM "core"."workspace" workspace
INNER JOIN "core"."application" standardApplication
ON standardApplication."workspaceId" = workspace."id"
AND standardApplication."universalIdentifier" = $7
AND standardApplication."deletedAt" IS NULL
ON CONFLICT ("key", "workspaceId") DO NOTHING`,
[
definition.universalIdentifier,
definition.key,
definition.label,
definition.description,
definition.icon,
definition.permissionType,
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
],
);
}
await dataSource.query(
`UPDATE "core"."rolePermissionFlag" rolePermissionFlag
SET "permissionFlagId" = permissionFlag."id"
FROM "core"."permissionFlag" permissionFlag
WHERE permissionFlag."workspaceId" = rolePermissionFlag."workspaceId"
AND permissionFlag."key" = rolePermissionFlag."flag"
AND rolePermissionFlag."permissionFlagId" IS NULL`,
);
}
public async up(_queryRunner: QueryRunner): Promise<void> {}
public async down(_queryRunner: QueryRunner): Promise<void> {}
}
@@ -31,6 +31,10 @@ import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/
import { BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier';
import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings';
import { AddMetadataToBillingPriceFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1777100000000-add-metadata-to-billing-price';
import { RenamePermissionFlagToRolePermissionFlagFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1778235340020-rename-permission-flag-to-role-permission-flag';
import { PermissionFlagSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1778235340021-permission-flag-syncable-entity';
import { LinkRolePermissionFlagToPermissionFlagFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1778235340022-link-role-permission-flag-to-permission-flag';
import { BackfillRolePermissionFlagPermissionFlagIdSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-slow-1778235340023-backfill-role-permission-flag-permission-flag-id';
import { AddEmailGroupChannelTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1778256809018-add-email-group-channel-type';
import { AddApplicationIdToPublicDomainFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain';
import { AddIsInternalMessagesImportEnabledFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1778525104406-add-is-internal-messages-import-enabled';
@@ -77,6 +81,10 @@ export const INSTANCE_COMMANDS = [
RemoveUserDefaultAvatarUrlFastInstanceCommand,
TransformApplicationVariableToSyncableEntityFastInstanceCommand,
BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand,
RenamePermissionFlagToRolePermissionFlagFastInstanceCommand,
PermissionFlagSyncableEntityFastInstanceCommand,
LinkRolePermissionFlagToPermissionFlagFastInstanceCommand,
BackfillRolePermissionFlagPermissionFlagIdSlowInstanceCommand,
AddEmailGroupChannelTypeFastInstanceCommand,
AddApplicationIdToPublicDomainFastInstanceCommand,
AddIsInternalMessagesImportEnabledFastInstanceCommand,
@@ -1,23 +0,0 @@
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
import { PermissionFlagManifest } from 'twenty-shared/application';
export const fromPermissionFlagToUniversalFlatPermissionFlag = ({
permissionFlag,
roleUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
permissionFlag: PermissionFlagManifest;
roleUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatPermissionFlag => {
return {
universalIdentifier: permissionFlag.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
flag: permissionFlag.flag,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,27 @@
import { type PermissionFlagManifest } from 'twenty-shared/application';
import { SystemPermissionFlag } from 'twenty-shared/constants';
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
export const fromPermissionFlagToUniversalFlatRolePermissionFlag = ({
permissionFlag,
roleUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
permissionFlag: PermissionFlagManifest;
roleUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatRolePermissionFlag => {
return {
universalIdentifier: permissionFlag.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
permissionFlagUniversalIdentifier:
SystemPermissionFlag[permissionFlag.flag],
flag: permissionFlag.flag,
createdAt: now,
updatedAt: now,
};
};
@@ -32,7 +32,7 @@ export const fromRoleManifestToUniversalFlatRole = ({
roleTargetUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
rolePermissionFlagUniversalIdentifiers: [],
rowLevelPermissionPredicateUniversalIdentifiers: [],
rowLevelPermissionPredicateGroupUniversalIdentifiers: [],
createdAt: now,
@@ -17,7 +17,7 @@ import { fromObjectPermissionManifestToUniversalFlatObjectPermission } from 'src
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromPermissionFlagToUniversalFlatPermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-permission-flag.util';
import { fromPermissionFlagToUniversalFlatRolePermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-role-permission-flag.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
@@ -214,14 +214,15 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
for (const permissionFlag of roleManifest.permissionFlags ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromPermissionFlagToUniversalFlatPermissionFlag({
permissionFlag,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntity:
fromPermissionFlagToUniversalFlatRolePermissionFlag({
permissionFlag,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPermissionFlagMaps,
allUniversalFlatEntityMaps.flatRolePermissionFlagMaps,
});
}
}
@@ -229,8 +229,11 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
},
"permissionFlag": {
"propertiesToCompare": [
"flag",
"roleUniversalIdentifier",
"key",
"label",
"description",
"icon",
"permissionType",
],
"propertiesToStringify": [],
},
@@ -251,6 +254,14 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
],
"propertiesToStringify": [],
},
"rolePermissionFlag": {
"propertiesToCompare": [
"permissionFlagUniversalIdentifier",
"roleUniversalIdentifier",
"flag",
],
"propertiesToStringify": [],
},
"roleTarget": {
"propertiesToCompare": [
"roleUniversalIdentifier",
@@ -1186,17 +1186,59 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: 'pageLayoutUniversalIdentifier',
},
},
permissionFlag: {
flag: {
rolePermissionFlag: {
permissionFlagId: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
universalProperty: 'permissionFlagUniversalIdentifier',
},
roleId: {
toCompare: true,
toStringify: false,
universalProperty: 'roleUniversalIdentifier',
},
flag: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
updatedAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
},
permissionFlag: {
key: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
label: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
description: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
icon: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
permissionType: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
@@ -171,12 +171,19 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
workspace: null,
application: null,
},
permissionFlag: {
rolePermissionFlag: {
workspace: null,
application: null,
role: {
foreignKey: 'roleId',
},
permissionFlag: {
foreignKey: 'permissionFlagId',
},
},
permissionFlag: {
workspace: null,
application: null,
},
objectPermission: {
workspace: null,
@@ -300,16 +300,27 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
workspace: null,
application: null,
},
permissionFlag: {
rolePermissionFlag: {
workspace: null,
application: null,
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'permissionFlags',
inverseOneToManyProperty: 'rolePermissionFlags',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
permissionFlag: {
metadataName: 'permissionFlag',
foreignKey: 'permissionFlagId',
inverseOneToManyProperty: 'rolePermissionFlags',
isNullable: true,
universalForeignKey: 'permissionFlagUniversalIdentifier',
},
},
permissionFlag: {
workspace: null,
application: null,
},
objectPermission: {
workspace: null,
@@ -17,6 +17,7 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -56,6 +57,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
agent: AgentEntity,
commandMenuItem: CommandMenuItemEntity,
navigationMenuItem: NavigationMenuItemEntity,
rolePermissionFlag: RolePermissionFlagEntity,
permissionFlag: PermissionFlagEntity,
webhook: WebhookEntity,
applicationVariable: ApplicationVariableEntity,
@@ -76,9 +76,13 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
view: true,
pageLayout: true,
},
permissionFlag: {
rolePermissionFlag: {
permissionFlag: true,
role: true,
},
permissionFlag: {
rolePermissionFlag: true,
},
objectPermission: {
role: true,
objectMetadata: true,
@@ -33,6 +33,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
logicFunction: {},
role: {},
roleTarget: {},
rolePermissionFlag: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
@@ -168,11 +168,11 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'objectPermissionUniversalIdentifiers',
},
permissionFlags: {
metadataName: 'permissionFlag',
flatEntityForeignKeyAggregator: 'permissionFlagIds',
rolePermissionFlags: {
metadataName: 'rolePermissionFlag',
flatEntityForeignKeyAggregator: 'rolePermissionFlagIds',
universalFlatEntityForeignKeyAggregator:
'permissionFlagUniversalIdentifiers',
'rolePermissionFlagUniversalIdentifiers',
},
fieldPermissions: {
metadataName: 'fieldPermission',
@@ -194,7 +194,15 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
},
},
roleTarget: {},
permissionFlag: {},
rolePermissionFlag: {},
permissionFlag: {
rolePermissionFlags: {
metadataName: 'rolePermissionFlag',
flatEntityForeignKeyAggregator: 'rolePermissionFlagIds',
universalFlatEntityForeignKeyAggregator:
'rolePermissionFlagUniversalIdentifiers',
},
},
objectPermission: {},
fieldPermission: {},
pageLayout: {
@@ -13,6 +13,7 @@ import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-m
import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service';
import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service';
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
import { WorkspaceFlatRolePermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-role-permission-flag/services/workspace-flat-role-permission-flag-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service';
import { WorkspaceFlatViewFieldGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service';
@@ -32,6 +33,7 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -64,6 +66,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
PageLayoutEntity,
PageLayoutTabEntity,
PageLayoutWidgetEntity,
RolePermissionFlagEntity,
PermissionFlagEntity,
RowLevelPermissionPredicateEntity,
RowLevelPermissionPredicateGroupEntity,
@@ -85,6 +88,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatRolePermissionFlagMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -106,6 +110,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatRolePermissionFlagMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -26,6 +26,8 @@ import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-l
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type FlatRolePermissionFlagMaps } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag-maps.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
@@ -70,6 +72,7 @@ import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/wo
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
import { type UniversalFlatRoleTarget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-target.type';
import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type';
import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type';
@@ -204,6 +207,14 @@ import {
type UniversalDeletePermissionFlagAction,
type UniversalUpdatePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import {
type FlatCreateRolePermissionFlagAction,
type FlatDeleteRolePermissionFlagAction,
type FlatUpdateRolePermissionFlagAction,
type UniversalCreateRolePermissionFlagAction,
type UniversalDeleteRolePermissionFlagAction,
type UniversalUpdateRolePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import {
type FlatCreateRoleTargetAction,
type FlatDeleteRoleTargetAction,
@@ -625,6 +636,22 @@ export type AllFlatEntityTypesByMetadataName = {
universalFlatEntity: UniversalFlatNavigationMenuItem;
entity: NavigationMenuItemEntity;
};
rolePermissionFlag: {
flatEntityMaps: FlatRolePermissionFlagMaps;
universalActions: {
create: UniversalCreateRolePermissionFlagAction;
update: UniversalUpdateRolePermissionFlagAction;
delete: UniversalDeleteRolePermissionFlagAction;
};
flatActions: {
create: FlatCreateRolePermissionFlagAction;
update: FlatUpdateRolePermissionFlagAction;
delete: FlatDeleteRolePermissionFlagAction;
};
flatEntity: FlatRolePermissionFlag;
universalFlatEntity: UniversalFlatRolePermissionFlag;
entity: MetadataEntity<'rolePermissionFlag'>;
};
permissionFlag: {
flatEntityMaps: FlatPermissionFlagMaps;
universalActions: {
@@ -93,7 +93,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
exports[`getMetadataRelatedMetadataNames should return related metadata names for permissionFlag 1`] = `
[
"role",
"rolePermissionFlag",
]
`;
@@ -101,13 +101,20 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
[
"roleTarget",
"objectPermission",
"permissionFlag",
"rolePermissionFlag",
"fieldPermission",
"rowLevelPermissionPredicate",
"rowLevelPermissionPredicateGroup",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for rolePermissionFlag 1`] = `
[
"role",
"permissionFlag",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for roleTarget 1`] = `
[
"role",
@@ -10,10 +10,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewField",
"objectPermission",
"pageLayoutWidget",
"rolePermissionFlag",
"viewSort",
"index",
"pageLayout",
"permissionFlag",
"roleTarget",
"rowLevelPermissionPredicateGroup",
"viewGroup",
@@ -27,6 +27,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewFieldGroup",
"viewFilterGroup",
"webhook",
"permissionFlag",
"view",
"fieldMetadata",
"objectMetadata",
@@ -1,6 +1,8 @@
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
export const FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES = [
'flag',
'roleId',
'label',
'description',
'icon',
'permissionType',
] as const satisfies MetadataEntityPropertyName<'permissionFlag'>[];
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
ApplicationEntity,
PermissionFlagEntity,
RolePermissionFlagEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatPermissionFlagMapCacheService],
exports: [WorkspaceFlatPermissionFlagMapCacheService],
})
export class FlatPermissionFlagModule {}
@@ -8,10 +8,11 @@ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-enti
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { fromPermissionFlagEntityToFlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-permission-flag-entity-to-flat-permission-flag.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -22,46 +23,52 @@ export class WorkspaceFlatPermissionFlagMapCacheService extends WorkspaceCachePr
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatPermissionFlagMaps> {
const [permissionFlags, applications, roles] = await Promise.all([
this.permissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const [permissionFlags, applications, rolePermissionFlags] =
await Promise.all([
this.permissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.rolePermissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const rolePermissionFlagsByPermissionFlagId =
regroupEntitiesByRelatedEntityId<'rolePermissionFlag'>({
entities: rolePermissionFlags,
foreignKey: 'permissionFlagId',
});
const flatPermissionFlagMaps = createEmptyFlatEntityMaps();
for (const permissionFlagEntity of permissionFlags) {
const flatPermissionFlag = fromPermissionFlagEntityToFlatPermissionFlag({
entity: permissionFlagEntity,
for (const definition of permissionFlags) {
const flatDefinition = fromPermissionFlagEntityToFlatPermissionFlag({
entity: {
...definition,
rolePermissionFlags:
rolePermissionFlagsByPermissionFlagId.get(definition.id) ?? [],
},
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatPermissionFlag,
flatEntity: flatDefinition,
flatEntityMapsToMutate: flatPermissionFlagMaps,
});
}
@@ -1,38 +1,44 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type CreatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
export const fromCreatePermissionFlagInputToFlatPermissionFlagToCreate = ({
createPermissionFlagInput,
workspaceId,
flatApplication,
flatRoleMaps,
}: {
createPermissionFlagInput: CreatePermissionFlagInput;
workspaceId: string;
flatApplication: FlatApplication;
} & Pick<AllFlatEntityMaps, 'flatRoleMaps'>): UniversalFlatPermissionFlag & {
id: string;
} => {
const { roleId, flag, universalIdentifier } = createPermissionFlagInput;
}): FlatPermissionFlag => {
const now = new Date().toISOString();
const { roleUniversalIdentifier } = resolveEntityRelationUniversalIdentifiers(
{
metadataName: 'permissionFlag',
foreignKeyValues: { roleId },
flatEntityMaps: { flatRoleMaps },
},
);
const { key, label, description, icon } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
createPermissionFlagInput,
['key', 'label', 'description', 'icon'],
);
const id = createPermissionFlagInput.id ?? v4();
const universalIdentifier =
createPermissionFlagInput.universalIdentifier ?? id;
return {
id: v4(),
flag,
universalIdentifier: universalIdentifier ?? v4(),
id,
universalIdentifier,
key,
label,
description: description ?? null,
icon: icon ?? null,
permissionType: createPermissionFlagInput.permissionType,
workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
roleUniversalIdentifier,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
};
@@ -0,0 +1,31 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
PermissionFlagException,
PermissionFlagExceptionCode,
} from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
export const fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow = ({
flatPermissionFlagMaps,
permissionFlagId,
}: {
flatPermissionFlagMaps: FlatEntityMaps<FlatPermissionFlag>;
permissionFlagId: string;
}): FlatPermissionFlag => {
const existing = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: permissionFlagId,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(existing)) {
throw new PermissionFlagException(
'Permission flag not found',
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
);
}
return existing;
};
@@ -0,0 +1,18 @@
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
export const fromFlatPermissionFlagToPermissionFlagDto = (
flatDefinition: FlatPermissionFlag,
): PermissionFlagDTO => ({
id: flatDefinition.id,
universalIdentifier: flatDefinition.universalIdentifier,
key: flatDefinition.key,
label: flatDefinition.label,
description: flatDefinition.description,
icon: flatDefinition.icon,
permissionType: flatDefinition.permissionType,
workspaceId: flatDefinition.workspaceId,
applicationId: flatDefinition.applicationId,
createdAt: new Date(flatDefinition.createdAt),
updatedAt: new Date(flatDefinition.updatedAt),
});
@@ -11,9 +11,8 @@ import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/type
export const fromPermissionFlagEntityToFlatPermissionFlag = ({
entity: permissionFlagEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'permissionFlag'>): FlatPermissionFlag => {
const permissionFlagEntityWithoutRelations = removePropertiesFromRecord(
const entityWithoutRelations = removePropertiesFromRecord(
permissionFlagEntity,
getMetadataEntityRelationProperties('permissionFlag'),
);
@@ -30,24 +29,18 @@ export const fromPermissionFlagEntityToFlatPermissionFlag = ({
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
permissionFlagEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${permissionFlagEntity.roleId} not found for permissionFlag ${permissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...permissionFlagEntityWithoutRelations,
...entityWithoutRelations,
createdAt: permissionFlagEntity.createdAt.toISOString(),
updatedAt: permissionFlagEntity.updatedAt.toISOString(),
universalIdentifier:
permissionFlagEntityWithoutRelations.universalIdentifier,
universalIdentifier: entityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
rolePermissionFlagIds: permissionFlagEntity.rolePermissionFlags.map(
({ id }) => id,
),
rolePermissionFlagUniversalIdentifiers:
permissionFlagEntity.rolePermissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
};
};
@@ -0,0 +1,42 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-permission-flag/constants/flat-permission-flag-editable-properties.constant';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
PermissionFlagException,
PermissionFlagExceptionCode,
} from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
import { type UpdatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/update-permission-flag.input';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow =
({
flatPermissionFlagMaps,
updatePermissionFlagInput,
}: {
flatPermissionFlagMaps: FlatEntityMaps<FlatPermissionFlag>;
updatePermissionFlagInput: UpdatePermissionFlagInput;
}): FlatPermissionFlag => {
const existing = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updatePermissionFlagInput.id,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(existing)) {
throw new PermissionFlagException(
'Permission flag not found',
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
);
}
return {
...mergeUpdateInExistingRecord({
existing,
properties: [...FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES],
update: updatePermissionFlagInput.update,
}),
updatedAt: new Date().toISOString(),
};
};
@@ -0,0 +1,57 @@
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
STANDARD_PERMISSION_FLAG_DEFINITIONS,
type StandardPermissionFlagDefinition,
} from 'src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant';
const STANDARD_PERMISSION_FLAG_DEFINITION_BY_KEY: Record<
PermissionFlagType,
StandardPermissionFlagDefinition
> = STANDARD_PERMISSION_FLAG_DEFINITIONS.reduce(
(accumulator, definition) => {
accumulator[definition.key] = definition;
return accumulator;
},
{} as Record<PermissionFlagType, StandardPermissionFlagDefinition>,
);
const SYNTHESIZED_TIMESTAMP = new Date(0).toISOString();
// TODO: Remove this once we have a proper permission flag catalog backfilled
export const synthesizeFlatPermissionFlagFromFlag = ({
flag,
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
flag: PermissionFlagType;
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): FlatPermissionFlag => {
const definition = STANDARD_PERMISSION_FLAG_DEFINITION_BY_KEY[flag];
const universalIdentifier = SystemPermissionFlag[flag];
return {
id: universalIdentifier,
universalIdentifier,
workspaceId,
applicationId,
applicationUniversalIdentifier,
key: flag,
label: definition.label,
description: definition.description,
icon: definition.icon,
permissionType: definition.permissionType,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: SYNTHESIZED_TIMESTAMP,
updatedAt: SYNTHESIZED_TIMESTAMP,
};
};
@@ -0,0 +1,86 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatRolePermissionFlagMaps } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag-maps.type';
import { fromRolePermissionFlagEntityToFlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/utils/from-role-permission-flag-entity-to-flat-role-permission-flag.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatRolePermissionFlagMaps')
export class WorkspaceFlatRolePermissionFlagMapCacheService extends WorkspaceCacheProvider<FlatRolePermissionFlagMaps> {
constructor(
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatRolePermissionFlagMaps> {
const [rolePermissionFlags, applications, roles, permissionFlags] =
await Promise.all([
this.rolePermissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.permissionFlagRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const permissionFlagIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(permissionFlags);
const flatRolePermissionFlagMaps = createEmptyFlatEntityMaps();
for (const rolePermissionFlagEntity of rolePermissionFlags) {
const flatRolePermissionFlag =
fromRolePermissionFlagEntityToFlatRolePermissionFlag({
entity: rolePermissionFlagEntity,
applicationIdToUniversalIdentifierMap,
permissionFlagIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRolePermissionFlag,
flatEntityMapsToMutate: flatRolePermissionFlagMaps,
});
}
return flatRolePermissionFlagMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
export type FlatRolePermissionFlagMaps = FlatEntityMaps<FlatRolePermissionFlag>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
export type FlatRolePermissionFlag = FlatEntityFrom<RolePermissionFlagEntity>;
@@ -0,0 +1,45 @@
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { type CreateRolePermissionFlagInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/create-role-permission-flag.input';
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
export const fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate =
({
createRolePermissionFlagInput,
flatApplication,
flatPermissionFlagMaps,
flatRoleMaps,
}: {
createRolePermissionFlagInput: CreateRolePermissionFlagInput;
flatApplication: FlatApplication;
} & Pick<
AllFlatEntityMaps,
'flatPermissionFlagMaps' | 'flatRoleMaps'
>): UniversalFlatRolePermissionFlag & {
id: string;
} => {
const { roleId, permissionFlagId, flag, universalIdentifier } =
createRolePermissionFlagInput;
const now = new Date().toISOString();
const { permissionFlagUniversalIdentifier, roleUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'rolePermissionFlag',
foreignKeyValues: { permissionFlagId, roleId },
flatEntityMaps: { flatPermissionFlagMaps, flatRoleMaps },
});
return {
id: v4(),
universalIdentifier: universalIdentifier ?? v4(),
applicationUniversalIdentifier: flatApplication.universalIdentifier,
permissionFlagUniversalIdentifier,
roleUniversalIdentifier,
flag,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,71 @@
import { SystemPermissionFlag } from 'twenty-shared/constants';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromRolePermissionFlagEntityToFlatRolePermissionFlag = ({
entity: rolePermissionFlagEntity,
applicationIdToUniversalIdentifierMap,
permissionFlagIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'rolePermissionFlag'>): FlatRolePermissionFlag => {
const rolePermissionFlagEntityWithoutRelations = removePropertiesFromRecord(
rolePermissionFlagEntity,
getMetadataEntityRelationProperties('rolePermissionFlag'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${rolePermissionFlagEntity.applicationId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${rolePermissionFlagEntity.roleId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const permissionFlagUniversalIdentifier = isDefined(
rolePermissionFlagEntity.permissionFlagId,
)
? permissionFlagIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.permissionFlagId,
)
: SystemPermissionFlag[rolePermissionFlagEntity.flag];
if (!isDefined(permissionFlagUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PermissionFlag with id ${rolePermissionFlagEntity.permissionFlagId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...rolePermissionFlagEntityWithoutRelations,
createdAt: rolePermissionFlagEntity.createdAt.toISOString(),
updatedAt: rolePermissionFlagEntity.updatedAt.toISOString(),
universalIdentifier:
rolePermissionFlagEntityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
permissionFlagUniversalIdentifier,
roleUniversalIdentifier,
};
};
@@ -2,8 +2,8 @@ import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/typ
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export type FlatRole = FlatEntityFrom<RoleEntity> & {
// TODO remove once objectPermission permissionFlag fieldPermission have been migrated to v2
// TODO remove once objectPermission rolePermissionFlag fieldPermission have been migrated to v2
objectPermissionIds: string[];
permissionFlagIds: string[];
rolePermissionFlagIds: string[];
fieldPermissionIds: string[];
};
@@ -54,9 +54,9 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
roleTargetIds: [],
roleTargetUniversalIdentifiers: [],
objectPermissionIds: [],
permissionFlagIds: [],
rolePermissionFlagIds: [],
objectPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
rolePermissionFlagUniversalIdentifiers: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
rowLevelPermissionPredicateIds: [],
@@ -13,10 +13,10 @@ import { type RegroupedEntity } from 'src/engine/workspace-cache/utils/regroup-e
type FromRoleEntityToFlatRoleArgs = {
entity: Omit<
EntityWithRegroupedOneToManyRelations<MetadataEntity<'role'>>,
'objectPermissions' | 'permissionFlags' | 'fieldPermissions'
'objectPermissions' | 'rolePermissionFlags' | 'fieldPermissions'
> & {
objectPermissions: RegroupedEntity[];
permissionFlags: RegroupedEntity[];
rolePermissionFlags: RegroupedEntity[];
fieldPermissions: RegroupedEntity[];
};
} & EntityManyToOneIdByUniversalIdentifierMaps<'role'>;
@@ -57,7 +57,7 @@ export const fromRoleEntityToFlatRole = ({
applicationId: roleEntity.applicationId,
roleTargetIds: roleEntity.roleTargets.map(({ id }) => id),
objectPermissionIds: roleEntity.objectPermissions.map(({ id }) => id),
permissionFlagIds: roleEntity.permissionFlags.map(({ id }) => id),
rolePermissionFlagIds: roleEntity.rolePermissionFlags.map(({ id }) => id),
fieldPermissionIds: roleEntity.fieldPermissions.map(({ id }) => id),
rowLevelPermissionPredicateIds: roleEntity.rowLevelPermissionPredicates.map(
({ id }) => id,
@@ -71,7 +71,7 @@ export const fromRoleEntityToFlatRole = ({
objectPermissionUniversalIdentifiers: roleEntity.objectPermissions.map(
({ universalIdentifier }) => universalIdentifier,
),
permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map(
rolePermissionFlagUniversalIdentifiers: roleEntity.rolePermissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
fieldPermissionUniversalIdentifiers: roleEntity.fieldPermissions.map(
@@ -17,6 +17,7 @@ import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/messag
import { MessageFolderMetadataModule } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.module';
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { MinimalMetadataModule } from 'src/engine/metadata-modules/minimal-metadata/minimal-metadata.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
@@ -47,6 +48,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
WorkspaceMetadataVersionModule,
RoleModule,
PermissionsModule,
PermissionFlagModule,
RouteTriggerModule,
WebhookModule,
ConnectedAccountMetadataModule,
@@ -75,6 +77,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
ViewModule,
RoleModule,
PermissionsModule,
PermissionFlagModule,
WebhookModule,
ConnectedAccountMetadataModule,
MessageChannelMetadataModule,
@@ -0,0 +1,4 @@
export const PERMISSION_FLAG_PERMISSION_TYPES = ['settings', 'tool'] as const;
export type PermissionFlagPermissionType =
(typeof PERMISSION_FLAG_PERMISSION_TYPES)[number];
@@ -0,0 +1,160 @@
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
export type StandardPermissionFlagDefinition = {
key: PermissionFlagType;
universalIdentifier: string;
label: string;
description: string | null;
icon: string | null;
permissionType: PermissionFlagPermissionType;
};
type StandardPermissionFlagMetadata = Pick<
StandardPermissionFlagDefinition,
'label' | 'description' | 'icon'
>;
const STANDARD_PERMISSION_FLAG_METADATA: Record<
PermissionFlagType,
StandardPermissionFlagMetadata
> = {
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: {
label: 'API Keys & Webhooks',
description: 'Manage API keys and webhooks',
icon: 'IconCode',
},
[PermissionFlagType.WORKSPACE]: {
label: 'Workspace',
description: 'Set global workspace preferences',
icon: 'IconSettings',
},
[PermissionFlagType.WORKSPACE_MEMBERS]: {
label: 'Users',
description: 'Add or remove users',
icon: 'IconUsers',
},
[PermissionFlagType.ROLES]: {
label: 'Roles',
description: 'Define user roles and access levels',
icon: 'IconLockOpen',
},
[PermissionFlagType.DATA_MODEL]: {
label: 'Data Model',
description: 'Edit data structure and fields',
icon: 'IconHierarchy',
},
[PermissionFlagType.SECURITY]: {
label: 'Security',
description: 'Manage security policies',
icon: 'IconKey',
},
[PermissionFlagType.WORKFLOWS]: {
label: 'Workflows',
description: 'Manage workflows',
icon: 'IconSettingsAutomation',
},
[PermissionFlagType.IMPERSONATE]: {
label: 'Impersonate',
description: 'Impersonate workspace users',
icon: 'IconSpy',
},
[PermissionFlagType.SSO_BYPASS]: {
label: 'SSO Bypass',
description: 'Enable bypass options',
icon: 'IconShield',
},
[PermissionFlagType.APPLICATIONS]: {
label: 'Applications',
description: 'Install and manage applications',
icon: 'IconApps',
},
[PermissionFlagType.MARKETPLACE_APPS]: {
label: 'Marketplace Apps',
description: 'Browse and install marketplace apps',
icon: 'IconShoppingBag',
},
[PermissionFlagType.LAYOUTS]: {
label: 'Layouts',
description: 'Customize page layouts and UI structure',
icon: 'IconLayoutSidebarRightCollapse',
},
[PermissionFlagType.BILLING]: {
label: 'Billing',
description: 'Manage billing and subscriptions',
icon: 'IconCreditCard',
},
[PermissionFlagType.AI_SETTINGS]: {
label: 'AI',
description: 'Create and configure AI agents',
icon: 'IconSparkles',
},
[PermissionFlagType.AI]: {
label: 'Ask AI',
description: 'Chat with AI agents and use AI features',
icon: 'IconSparkles',
},
[PermissionFlagType.VIEWS]: {
label: 'Manage Views',
description: 'Create, edit, and delete workspace views',
icon: 'IconTable',
},
[PermissionFlagType.UPLOAD_FILE]: {
label: 'Upload Files',
description: 'Allow uploading files and attachments',
icon: 'IconFileUpload',
},
[PermissionFlagType.DOWNLOAD_FILE]: {
label: 'Download Files',
description: 'Allow downloading files and attachments',
icon: 'IconDownload',
},
[PermissionFlagType.SEND_EMAIL_TOOL]: {
label: 'Send Email',
description: 'Send emails via connected accounts',
icon: 'IconMail',
},
[PermissionFlagType.HTTP_REQUEST_TOOL]: {
label: 'HTTP Request',
description: 'Make HTTP requests to external APIs',
icon: 'IconApi',
},
[PermissionFlagType.CODE_INTERPRETER_TOOL]: {
label: 'Code Interpreter',
description: 'Run code to analyze files and data',
icon: 'IconCode',
},
[PermissionFlagType.IMPORT_CSV]: {
label: 'Import CSV',
description: 'Allow importing data from CSV files',
icon: 'IconFileImport',
},
[PermissionFlagType.EXPORT_CSV]: {
label: 'Export CSV',
description: 'Allow exporting data to CSV files',
icon: 'IconFileExport',
},
[PermissionFlagType.CONNECTED_ACCOUNTS]: {
label: 'Sync Account',
description: 'Sync email and calendar accounts',
icon: 'IconAt',
},
[PermissionFlagType.PROFILE_INFORMATION]: {
label: 'Edit Profile',
description: 'Edit own profile information',
icon: 'IconUser',
},
};
export const STANDARD_PERMISSION_FLAG_DEFINITIONS: StandardPermissionFlagDefinition[] =
Object.values(PermissionFlagType).map((key) => ({
key,
universalIdentifier: SystemPermissionFlag[key],
...STANDARD_PERMISSION_FLAG_METADATA[key],
permissionType: TOOL_PERMISSION_FLAGS.includes(key) ? 'tool' : 'settings',
}));
@@ -1,7 +1,52 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { Field, InputType } from '@nestjs/graphql';
export type CreatePermissionFlagInput = {
roleId: string;
flag: PermissionFlagType;
import {
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@InputType()
export class CreatePermissionFlagInput {
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
universalIdentifier?: string;
};
@IsString()
@IsNotEmpty()
@Field()
key: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagPermissionType;
}
@@ -1,17 +1,67 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
IsDateString,
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@ObjectType('PermissionFlag')
export class PermissionFlagDTO {
@Field(() => UUIDScalarType, { nullable: false })
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
roleId: string;
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
universalIdentifier: string;
@Field({ nullable: false })
flag: PermissionFlagType;
@IsString()
@IsNotEmpty()
@Field()
key: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
description: string | null;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
icon: string | null;
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagPermissionType;
@HideField()
workspaceId: string;
@Field(() => UUIDScalarType)
applicationId: string;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -0,0 +1,57 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@InputType()
export class UpdatePermissionFlagInputUpdates {
@IsOptional()
@IsString()
@Field({ nullable: true })
label?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
icon?: string;
@IsOptional()
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String, { nullable: true })
permissionType?: PermissionFlagPermissionType;
}
@InputType()
export class UpdatePermissionFlagInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType, {
description: 'The id of the permission flag definition to update',
})
id: string;
@Type(() => UpdatePermissionFlagInputUpdates)
@ValidateNested()
@Field(() => UpdatePermissionFlagInputUpdates, {
description: 'The fields to update',
})
update: UpdatePermissionFlagInputUpdates;
}
@@ -1,38 +1,46 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('permissionFlag')
@Unique('IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId'])
@Index('IDX_PERMISSION_FLAG_ROLE_ID', ['roleId'])
@Unique('IDX_PERMISSION_FLAG_KEY_WORKSPACE_ID_UNIQUE', ['key', 'workspaceId'])
@Index('IDX_PERMISSION_FLAG_APPLICATION_ID', ['applicationId'])
export class PermissionFlagEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.permissionFlags, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
key: string;
@Column({ nullable: false, type: 'varchar' })
flag: PermissionFlagType;
label: string;
@Column({ nullable: true, type: 'text' })
description: string | null;
@Column({ nullable: true, type: 'varchar' })
icon: string | null;
@Column({ nullable: false, type: 'varchar' })
permissionType: PermissionFlagPermissionType;
@OneToMany(
() => RolePermissionFlagEntity,
(rolePermissionFlag) => rolePermissionFlag.permissionFlag,
)
rolePermissionFlags: Relation<RolePermissionFlagEntity[]>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -0,0 +1,52 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum PermissionFlagExceptionCode {
PERMISSION_FLAG_NOT_FOUND = 'PERMISSION_FLAG_NOT_FOUND',
PERMISSION_FLAG_ALREADY_EXISTS = 'PERMISSION_FLAG_ALREADY_EXISTS',
INVALID_PERMISSION_FLAG_KEY = 'INVALID_PERMISSION_FLAG_KEY',
INVALID_PERMISSION_FLAG_PERMISSION_TYPE = 'INVALID_PERMISSION_FLAG_PERMISSION_TYPE',
PERMISSION_FLAG_KEY_IMMUTABLE = 'PERMISSION_FLAG_KEY_IMMUTABLE',
PERMISSION_FLAG_IS_STANDARD = 'PERMISSION_FLAG_IS_STANDARD',
PERMISSION_FLAG_IN_USE = 'PERMISSION_FLAG_IN_USE',
}
const getPermissionFlagExceptionUserFriendlyMessage = (
code: PermissionFlagExceptionCode,
) => {
switch (code) {
case PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND:
return msg`Permission flag not found.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS:
return msg`Permission flag already exists.`;
case PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_KEY:
return msg`Invalid permission flag key.`;
case PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE:
return msg`Invalid permission flag permission type.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_KEY_IMMUTABLE:
return msg`Permission flag key cannot be changed.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD:
return msg`Standard permission flags cannot be modified.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE:
return msg`Permission flag is still assigned to a role.`;
default:
assertUnreachable(code);
}
};
export class PermissionFlagException extends CustomException<PermissionFlagExceptionCode> {
constructor(
message: string,
code: PermissionFlagExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getPermissionFlagExceptionUserFriendlyMessage(code),
});
}
}
@@ -2,16 +2,20 @@ import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FlatPermissionFlagModule } from 'src/engine/metadata-modules/flat-permission-flag/flat-permission-flag.module';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceCacheModule,
ApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
WorkspaceCacheStorageModule,
ApplicationModule,
FlatPermissionFlagModule,
PermissionsModule,
],
providers: [PermissionFlagService],
exports: [PermissionFlagService],
@@ -1,19 +1,18 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { fromCreatePermissionFlagInputToFlatPermissionFlagToCreate } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-create-permission-flag-input-to-flat-permission-flag-to-create.util';
import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-delete-permission-flag-input-to-flat-permission-flag-or-throw.util';
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
import { fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-update-permission-flag-input-to-flat-permission-flag-to-update-or-throw.util';
import { type CreatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { type UpdatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/update-permission-flag.input';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@@ -25,115 +24,8 @@ export class PermissionFlagService {
private readonly applicationService: ApplicationService,
) {}
public async upsertPermissionFlags({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertPermissionFlagsInput;
}): Promise<FlatPermissionFlag[]> {
const { flatPermissionFlagMaps, flatRoleMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps', 'flatRoleMaps'],
},
);
const roleUniversalId = flatRoleMaps.universalIdentifierById[input.roleId];
const role = isDefined(roleUniversalId)
? flatRoleMaps.byUniversalIdentifier[roleUniversalId]
: undefined;
if (!isDefined(role)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are trying to modify could not be found.`,
},
);
}
const invalidFlags = input.permissionFlagKeys.filter(
(flag) => !Object.values(PermissionFlagType).includes(flag),
);
if (invalidFlags.length > 0) {
throw new PermissionsException(
`${PermissionsExceptionMessage.INVALID_SETTING}: ${invalidFlags.join(', ')}`,
PermissionsExceptionCode.INVALID_SETTING,
{
userFriendlyMessage: msg`Some of the permissions you selected are not valid. Please try again with valid permission settings.`,
},
);
}
const roleUniversalIdentifier = role.universalIdentifier;
const currentPermissionFlagsForRole = Object.values(
flatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatPermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
const inputSet = new Set(input.permissionFlagKeys);
const existingSet = new Set(
currentPermissionFlagsForRole.map((pf) => pf.flag),
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatEntityToCreate = input.permissionFlagKeys
.filter((flag) => !existingSet.has(flag))
.map((flag) =>
fromCreatePermissionFlagInputToFlatPermissionFlagToCreate({
createPermissionFlagInput: { roleId: input.roleId, flag },
flatApplication: workspaceCustomFlatApplication,
flatRoleMaps,
}),
);
const flatEntityToDelete = currentPermissionFlagsForRole.filter(
(pf) => !inputSet.has(pf.flag),
);
if (flatEntityToCreate.length === 0 && flatEntityToDelete.length === 0) {
const unchanged = currentPermissionFlagsForRole.filter((pf) =>
inputSet.has(pf.flag),
);
return unchanged;
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate,
flatEntityToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting permission flags',
);
}
const { flatPermissionFlagMaps: freshFlatPermissionFlagMaps } =
async findAll(workspaceId: string): Promise<PermissionFlagDTO[]> {
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -141,13 +33,196 @@ export class PermissionFlagService {
},
);
const resultFlags = Object.values(
freshFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatPermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
return Object.values(flatPermissionFlagMaps.byUniversalIdentifier)
.filter(isDefined)
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
.map(fromFlatPermissionFlagToPermissionFlagDto);
}
return resultFlags;
async findById(
id: string,
workspaceId: string,
): Promise<PermissionFlagDTO | null> {
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatPermissionFlag = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(flatPermissionFlag)) {
return null;
}
return fromFlatPermissionFlagToPermissionFlagDto(flatPermissionFlag);
}
async create(
input: CreatePermissionFlagInput,
workspaceId: string,
): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatToCreate =
fromCreatePermissionFlagInputToFlatPermissionFlagToCreate({
createPermissionFlagInput: input,
workspaceId,
flatApplication: workspaceCustomFlatApplication,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [flatToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while creating permission flag',
);
}
const { flatPermissionFlagMaps: recomputedMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return fromFlatPermissionFlagToPermissionFlagDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatToCreate.id,
flatEntityMaps: recomputedMaps,
}),
);
}
async update(
input: UpdatePermissionFlagInput,
workspaceId: string,
): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatPermissionFlagMaps: existingMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatToUpdate =
fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow({
flatPermissionFlagMaps: existingMaps,
updatePermissionFlagInput: input,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatToUpdate],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while updating permission flag',
);
}
const { flatPermissionFlagMaps: recomputedMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return fromFlatPermissionFlagToPermissionFlagDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: input.id,
flatEntityMaps: recomputedMaps,
}),
);
}
async delete(id: string, workspaceId: string): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatPermissionFlagMaps: existingMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatToDelete =
fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow({
flatPermissionFlagMaps: existingMaps,
permissionFlagId: id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [],
flatEntityToDelete: [flatToDelete],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while deleting permission flag',
);
}
return fromFlatPermissionFlagToPermissionFlagDto(flatToDelete);
}
}
@@ -1,10 +0,0 @@
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
export const fromFlatPermissionFlagToPermissionFlagDto = (
flatPermissionFlag: FlatPermissionFlag,
): PermissionFlagDTO => ({
id: flatPermissionFlag.id,
roleId: flatPermissionFlag.roleId,
flag: flatPermissionFlag.flag,
});
@@ -1,7 +1,10 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -60,7 +63,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
@@ -133,7 +136,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
@@ -184,7 +187,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -254,7 +257,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -292,8 +295,8 @@ describe('PermissionsService', () => {
});
});
describe('Granular permissions with permissionFlags', () => {
it('should grant specific tool permission when included in permissionFlags even if canAccessAllTools is false', () => {
describe('Granular permissions with rolePermissionFlags', () => {
it('should grant specific tool permission when included in rolePermissionFlags even if canAccessAllTools is false', () => {
const roleWithSpecificPermission: Partial<RoleEntity> = {
id: 'test-role-id',
label: 'Test Role',
@@ -308,10 +311,13 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [
rolePermissionFlags: [
{
id: 'permission-1',
flag: PermissionFlagType.UPLOAD_FILE,
permissionFlag: {
key: PermissionFlagType.UPLOAD_FILE,
universalIdentifier: SystemPermissionFlag.UPLOAD_FILE,
},
roleId: 'test-role-id',
workspaceId: 'test-workspace-id',
createdAt: new Date(),
@@ -338,7 +344,7 @@ describe('PermissionsService', () => {
).toBe(false);
});
it('should grant specific settings permission when included in permissionFlags even if canUpdateAllSettings is false', () => {
it('should grant specific settings permission when included in rolePermissionFlags even if canUpdateAllSettings is false', () => {
const roleWithSpecificPermission: Partial<RoleEntity> = {
id: 'test-role-id',
label: 'Test Role',
@@ -353,10 +359,13 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [
rolePermissionFlags: [
{
id: 'permission-1',
flag: PermissionFlagType.ROLES,
permissionFlag: {
key: PermissionFlagType.ROLES,
universalIdentifier: SystemPermissionFlag.ROLES,
},
roleId: 'test-role-id',
workspaceId: 'test-workspace-id',
createdAt: new Date(),
@@ -400,7 +409,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
@@ -66,7 +69,7 @@ export class PermissionsService {
const defaultSettingsPermissions =
this.getDefaultUserWorkspacePermissions().permissionFlags;
const permissionFlags = Object.keys(PermissionFlagType).reduce(
const permissionFlags = Object.values(PermissionFlagType).reduce(
(acc, feature) => {
const hasBasePermission = this.isToolPermission(feature)
? roleOfUserWorkspace.canAccessAllTools
@@ -76,9 +79,7 @@ export class PermissionsService {
...acc,
[feature]:
hasBasePermission ||
roleOfUserWorkspace.permissionFlags.some(
(permissionFlag) => permissionFlag.flag === feature,
),
this.roleHasPermissionFlag(roleOfUserWorkspace, feature),
};
},
defaultSettingsPermissions,
@@ -150,7 +151,10 @@ export class PermissionsService {
const role = await this.roleRepository.findOne({
where: { id: roleId, workspaceId },
relations: ['permissionFlags'],
relations: [
'rolePermissionFlags',
'rolePermissionFlags.permissionFlag',
],
});
if (!isDefined(role)) {
@@ -203,7 +207,10 @@ export class PermissionsService {
const role = await this.roleRepository.findOne({
where: { id: applicationRoleId, workspaceId },
relations: ['permissionFlags'],
relations: [
'rolePermissionFlags',
'rolePermissionFlags.permissionFlag',
],
});
if (!isDefined(role)) {
@@ -240,10 +247,22 @@ export class PermissionsService {
return true;
}
const permissionFlags = role.permissionFlags ?? [];
return this.roleHasPermissionFlag(role, setting);
}
return permissionFlags.some(
(permissionFlag) => permissionFlag.flag === setting,
private roleHasPermissionFlag(
role: RoleEntity,
flag: PermissionFlagType,
): boolean {
const rolePermissionFlags = role.rolePermissionFlags ?? [];
const permissionFlagUniversalIdentifier = SystemPermissionFlag[flag];
return rolePermissionFlags.some(
(rolePermissionFlag) =>
(rolePermissionFlag.permissionFlag?.universalIdentifier ??
SystemPermissionFlag[rolePermissionFlag.flag]) ===
permissionFlagUniversalIdentifier,
);
}
@@ -292,7 +311,7 @@ export class PermissionsService {
const result = await this.getRolesFromPermissionConfig(
rolePermissionConfig,
workspaceId,
['permissionFlags'],
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
);
if (result === null) {
@@ -318,7 +337,7 @@ export class PermissionsService {
const result = await this.getRolesFromPermissionConfig(
rolePermissionConfig,
workspaceId,
['permissionFlags'],
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
);
if (result === null) {
@@ -332,11 +351,7 @@ export class PermissionsService {
return true;
}
const permissionFlags = role.permissionFlags ?? [];
return permissionFlags.some(
(permissionFlag) => permissionFlag.flag === flag,
);
return this.roleHasPermissionFlag(role, flag);
};
return useIntersection
@@ -0,0 +1,8 @@
import { type PermissionFlagType } from 'twenty-shared/constants';
export type CreateRolePermissionFlagInput = {
roleId: string;
permissionFlagId: string;
flag: PermissionFlagType;
universalIdentifier?: string;
};
@@ -0,0 +1,17 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('RolePermissionFlag')
export class RolePermissionFlagDTO {
@Field(() => UUIDScalarType, { nullable: false })
id: string;
@Field(() => UUIDScalarType, { nullable: false })
roleId: string;
@Field({ nullable: false })
flag: PermissionFlagType;
}
@@ -0,0 +1,61 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('rolePermissionFlag')
@Unique('IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId'])
@Unique('IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID_ROLE_ID_UNIQUE', [
'permissionFlagId',
'roleId',
])
@Index('IDX_ROLE_PERMISSION_FLAG_ROLE_ID', ['roleId'])
@Index('IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID', ['permissionFlagId'])
export class RolePermissionFlagEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.rolePermissionFlags, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
flag: PermissionFlagType;
@Column({ nullable: true, type: 'uuid' })
permissionFlagId: string | null;
@ManyToOne(
() => PermissionFlagEntity,
(permissionFlag) => permissionFlag.rolePermissionFlags,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'permissionFlagId' })
permissionFlag: Relation<PermissionFlagEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RolePermissionFlagService } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.service';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceCacheModule,
ApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [RolePermissionFlagService],
exports: [RolePermissionFlagService],
})
export class RolePermissionFlagModule {}
@@ -0,0 +1,185 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import {
type PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate } from 'src/engine/metadata-modules/flat-role-permission-flag/utils/from-create-role-permission-flag-input-to-flat-role-permission-flag-to-create.util';
import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/upsert-permission-flags.input';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class RolePermissionFlagService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
) {}
public async upsertPermissionFlags({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertPermissionFlagsInput;
}): Promise<FlatRolePermissionFlag[]> {
const { flatPermissionFlagMaps, flatRolePermissionFlagMaps, flatRoleMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatPermissionFlagMaps',
'flatRolePermissionFlagMaps',
'flatRoleMaps',
],
},
);
const roleUniversalId = flatRoleMaps.universalIdentifierById[input.roleId];
const role = isDefined(roleUniversalId)
? flatRoleMaps.byUniversalIdentifier[roleUniversalId]
: undefined;
if (!isDefined(role)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are trying to modify could not be found.`,
},
);
}
const roleUniversalIdentifier = role.universalIdentifier;
const permissionFlagByKey = new Map(
Object.values(flatPermissionFlagMaps.byUniversalIdentifier)
.filter(isDefined)
.map((permissionFlag) => [permissionFlag.key, permissionFlag]),
);
const permissionFlagsByInputKey = input.permissionFlagKeys.map((flag) => ({
flag,
permissionFlag: permissionFlagByKey.get(flag),
}));
const missingPermissionFlags = permissionFlagsByInputKey
.filter(({ permissionFlag }) => !isDefined(permissionFlag))
.map(({ flag }) => flag);
if (missingPermissionFlags.length > 0) {
throw new PermissionsException(
`${PermissionsExceptionMessage.INVALID_SETTING}: ${missingPermissionFlags.join(', ')}`,
PermissionsExceptionCode.INVALID_SETTING,
{
userFriendlyMessage: msg`Some of the permissions you selected are not valid. Please try again with valid permission settings.`,
},
);
}
const currentRolePermissionFlagsForRole = Object.values(
flatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatRolePermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
const inputSet = new Set(
permissionFlagsByInputKey
.map(({ permissionFlag }) => permissionFlag?.universalIdentifier)
.filter(isDefined),
);
const getEffectiveUniversalIdentifier = (
pf: FlatRolePermissionFlag,
): string =>
pf.permissionFlagUniversalIdentifier ?? SystemPermissionFlag[pf.flag];
const existingSet = new Set(
currentRolePermissionFlagsForRole.map(getEffectiveUniversalIdentifier),
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatEntityToCreate = permissionFlagsByInputKey
.map(({ permissionFlag }) => permissionFlag)
.filter(isDefined)
.filter(
(permissionFlag) =>
!existingSet.has(permissionFlag.universalIdentifier),
)
.map((permissionFlag) =>
fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate({
createRolePermissionFlagInput: {
roleId: input.roleId,
permissionFlagId: permissionFlag.id,
flag: permissionFlag.key as PermissionFlagType,
},
flatApplication: workspaceCustomFlatApplication,
flatPermissionFlagMaps,
flatRoleMaps,
}),
);
const flatEntityToDelete = currentRolePermissionFlagsForRole.filter(
(pf) => !inputSet.has(getEffectiveUniversalIdentifier(pf)),
);
if (flatEntityToCreate.length === 0 && flatEntityToDelete.length === 0) {
return currentRolePermissionFlagsForRole.filter((pf) =>
inputSet.has(getEffectiveUniversalIdentifier(pf)),
);
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
rolePermissionFlag: {
flatEntityToCreate,
flatEntityToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting permission flags',
);
}
const { flatRolePermissionFlagMaps: freshFlatRolePermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRolePermissionFlagMaps'],
},
);
const resultFlags = Object.values(
freshFlatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatRolePermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
return resultFlags;
}
}
@@ -0,0 +1,79 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { fromFlatRolePermissionFlagToRolePermissionFlagDto } from 'src/engine/metadata-modules/role-permission-flag/utils/from-flat-role-permission-flag-to-role-permission-flag-dto.util';
const buildFlatRolePermissionFlag = (
overrides: Partial<FlatRolePermissionFlag> = {},
): FlatRolePermissionFlag =>
({
id: 'role-permission-flag-id',
roleId: 'role-id',
workspaceId: 'workspace-id',
applicationId: 'application-id',
applicationUniversalIdentifier: 'application-universal-id',
permissionFlagUniversalIdentifier: 'permission-flag-universal-id',
flag: PermissionFlagType.WORKSPACE,
...overrides,
}) as FlatRolePermissionFlag;
const buildFlatPermissionFlag = (): FlatPermissionFlag => ({
id: 'permission-flag-id',
workspaceId: 'workspace-id',
applicationId: 'application-id',
universalIdentifier: 'permission-flag-universal-id',
applicationUniversalIdentifier: 'application-universal-id',
key: PermissionFlagType.WORKSPACE,
label: 'Workspace',
description: null,
icon: null,
permissionType: 'settings',
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: '2026-05-13T00:00:00.000Z',
updatedAt: '2026-05-13T00:00:00.000Z',
});
const buildFlatPermissionFlagMaps = (): FlatPermissionFlagMaps => ({
byUniversalIdentifier: {
'permission-flag-universal-id': buildFlatPermissionFlag(),
},
universalIdentifierById: {
'permission-flag-id': 'permission-flag-universal-id',
},
universalIdentifiersByApplicationId: {},
});
describe('fromFlatRolePermissionFlagToRolePermissionFlagDto', () => {
it('maps role permission flags to the public permission flag DTO shape', () => {
expect(
fromFlatRolePermissionFlagToRolePermissionFlagDto(
buildFlatRolePermissionFlag(),
buildFlatPermissionFlagMaps(),
),
).toEqual({
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
});
});
it('falls back to the legacy flag column when the catalog is missing the entry', () => {
expect(
fromFlatRolePermissionFlagToRolePermissionFlagDto(
buildFlatRolePermissionFlag(),
{
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
),
).toEqual({
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
});
});
});
@@ -0,0 +1,36 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { synthesizeFlatPermissionFlagFromFlag } from 'src/engine/metadata-modules/flat-permission-flag/utils/synthesize-flat-permission-flag-from-flag.util';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
export const fromFlatRolePermissionFlagToRolePermissionFlagDto = (
flatRolePermissionFlag: FlatRolePermissionFlag,
flatPermissionFlagMaps: FlatPermissionFlagMaps,
): RolePermissionFlagDTO => {
const catalogPermissionFlag = isDefined(
flatRolePermissionFlag.permissionFlagUniversalIdentifier,
)
? flatPermissionFlagMaps.byUniversalIdentifier[
flatRolePermissionFlag.permissionFlagUniversalIdentifier
]
: undefined;
const permissionFlag = isDefined(catalogPermissionFlag)
? catalogPermissionFlag
: synthesizeFlatPermissionFlagFromFlag({
flag: flatRolePermissionFlag.flag,
workspaceId: flatRolePermissionFlag.workspaceId,
applicationId: flatRolePermissionFlag.applicationId,
applicationUniversalIdentifier:
flatRolePermissionFlag.applicationUniversalIdentifier,
});
return {
id: flatRolePermissionFlag.id,
roleId: flatRolePermissionFlag.roleId,
flag: permissionFlag.key as PermissionFlagType,
};
};
@@ -7,7 +7,7 @@ import { WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
import { type RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto';
import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto';
@@ -86,8 +86,8 @@ export class RoleDTO {
@Field({ nullable: false })
canDestroyAllObjectRecords: boolean;
@Field(() => [PermissionFlagDTO], { nullable: true })
permissionFlags?: PermissionFlagDTO[];
@Field(() => [RolePermissionFlagDTO], { nullable: true })
permissionFlags?: RolePermissionFlagDTO[];
@Field(() => [ObjectPermissionDTO], { nullable: true })
objectPermissions?: ObjectPermissionDTO[];
@@ -11,7 +11,7 @@ import {
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -81,10 +81,10 @@ export class RoleEntity extends SyncableEntity implements Required<RoleEntity> {
objectPermissions: Relation<ObjectPermissionEntity[]>;
@OneToMany(
() => PermissionFlagEntity,
(permissionFlag: PermissionFlagEntity) => permissionFlag.role,
() => RolePermissionFlagEntity,
(rolePermissionFlag: RolePermissionFlagEntity) => rolePermissionFlag.role,
)
permissionFlags: Relation<PermissionFlagEntity[]>;
rolePermissionFlags: Relation<RolePermissionFlagEntity[]>;
@OneToMany(
() => FieldPermissionEntity,
@@ -15,8 +15,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RolePermissionFlagModule } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -39,7 +39,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
RoleEntity,
RoleTargetEntity,
ObjectPermissionEntity,
PermissionFlagEntity,
RolePermissionFlagEntity,
FieldPermissionEntity,
UserWorkspaceEntity,
ObjectMetadataEntity,
@@ -52,7 +52,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
ApiKeyModule,
PermissionsModule,
ObjectPermissionModule,
PermissionFlagModule,
RolePermissionFlagModule,
RowLevelPermissionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
@@ -33,16 +33,17 @@ import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto
import { fromFlatAgentWithRoleIdToAgentDto } from 'src/engine/metadata-modules/flat-agent/utils/from-agent-entity-to-agent-dto.util';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { fromFlatFieldPermissionToFieldPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-field-permission-to-field-permission-dto.util';
import { fromFlatObjectPermissionToObjectPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
import { RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/upsert-permission-flags.input';
import { RolePermissionFlagService } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.service';
import { fromFlatRolePermissionFlagToRolePermissionFlagDto } from 'src/engine/metadata-modules/role-permission-flag/utils/from-flat-role-permission-flag-to-role-permission-flag-dto.util';
import {
PermissionsException,
PermissionsExceptionCode,
@@ -87,7 +88,7 @@ export class RoleResolver {
private readonly roleService: RoleService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly objectPermissionService: ObjectPermissionService,
private readonly settingPermissionService: PermissionFlagService,
private readonly rolePermissionFlagService: RolePermissionFlagService,
private readonly agentRoleService: AiAgentRoleService,
private readonly apiKeyRoleService: ApiKeyRoleService,
private readonly fieldPermissionService: FieldPermissionService,
@@ -95,6 +96,7 @@ export class RoleResolver {
private readonly rowLevelPermissionPredicateService: RowLevelPermissionPredicateService,
private readonly rowLevelPermissionPredicateGroupService: RowLevelPermissionPredicateGroupService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@Query(() => [RoleDTO])
@@ -223,18 +225,31 @@ export class RoleResolver {
);
}
@Mutation(() => [PermissionFlagDTO])
@Mutation(() => [RolePermissionFlagDTO])
async upsertPermissionFlags(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('upsertPermissionFlagsInput')
upsertPermissionFlagsInput: UpsertPermissionFlagsInput,
): Promise<PermissionFlagDTO[]> {
const flatPermissionFlags =
await this.settingPermissionService.upsertPermissionFlags({
): Promise<RolePermissionFlagDTO[]> {
const flatRolePermissionFlags =
await this.rolePermissionFlagService.upsertPermissionFlags({
workspaceId: workspace.id,
input: upsertPermissionFlagsInput,
});
return flatPermissionFlags.map(fromFlatPermissionFlagToPermissionFlagDto);
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: workspace.id,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return flatRolePermissionFlags.map((flatRolePermissionFlag) =>
fromFlatRolePermissionFlagToRolePermissionFlagDto(
flatRolePermissionFlag,
flatPermissionFlagMaps,
),
);
}
@Mutation(() => [FieldPermissionDTO])
@@ -49,7 +49,9 @@ export class RoleService {
},
relations: {
roleTargets: true,
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
objectPermissions: true,
fieldPermissions: true,
},
@@ -67,7 +69,9 @@ export class RoleService {
},
relations: {
roleTargets: true,
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
objectPermissions: true,
fieldPermissions: true,
},
@@ -1,14 +1,17 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -23,7 +26,7 @@ const PERSON_OBJECT_METADATA_ID = '44444444-4444-4444-8444-444444444444';
const createBaseRole = (
overrides: Partial<RoleEntity> &
Pick<RoleEntity, 'permissionFlags' | 'objectPermissions'>,
Pick<RoleEntity, 'rolePermissionFlags' | 'objectPermissions'>,
): RoleEntity =>
({
id: ROLE_ID,
@@ -56,8 +59,8 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
let objectPermissionRepository: jest.Mocked<
Pick<Repository<ObjectPermissionEntity>, 'find'>
>;
let permissionFlagRepository: jest.Mocked<
Pick<Repository<PermissionFlagEntity>, 'find'>
let rolePermissionFlagRepository: jest.Mocked<
Pick<Repository<RolePermissionFlagEntity>, 'find'>
>;
const workspaceObjectMetadataFixture: ObjectMetadataEntity[] = [
@@ -94,7 +97,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
find: jest.fn().mockResolvedValue([]),
};
permissionFlagRepository = {
rolePermissionFlagRepository = {
find: jest.fn().mockResolvedValue([]),
};
const fieldPermissionRepository = {
@@ -123,8 +126,8 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
useValue: objectPermissionRepository,
},
{
provide: getRepositoryToken(PermissionFlagEntity),
useValue: permissionFlagRepository,
provide: getRepositoryToken(RolePermissionFlagEntity),
useValue: rolePermissionFlagRepository,
},
{
provide: getRepositoryToken(FieldPermissionEntity),
@@ -148,7 +151,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
it('should deny all record permissions when role has neither workspace members access nor update-all-settings', async () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -164,16 +167,19 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
});
it('should grant all record permissions when role has WORKSPACE_MEMBERS permission flag', async () => {
permissionFlagRepository.find.mockResolvedValue([
rolePermissionFlagRepository.find.mockResolvedValue([
{
roleId: ROLE_ID,
flag: PermissionFlagType.WORKSPACE_MEMBERS,
} as PermissionFlagEntity,
permissionFlag: {
key: PermissionFlagType.WORKSPACE_MEMBERS,
universalIdentifier: SystemPermissionFlag.WORKSPACE_MEMBERS,
},
} as RolePermissionFlagEntity,
]);
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -192,7 +198,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
canUpdateAllSettings: true,
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -210,7 +216,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
it('should deny all record permissions when role has neither workflows access nor update-all-settings', async () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -225,16 +231,19 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
});
it('should grant all record permissions when role has WORKFLOWS permission flag', async () => {
permissionFlagRepository.find.mockResolvedValue([
rolePermissionFlagRepository.find.mockResolvedValue([
{
roleId: ROLE_ID,
flag: PermissionFlagType.WORKFLOWS,
} as PermissionFlagEntity,
permissionFlag: {
key: PermissionFlagType.WORKFLOWS,
universalIdentifier: SystemPermissionFlag.WORKFLOWS,
},
} as RolePermissionFlagEntity,
]);
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -264,7 +273,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -285,7 +294,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -12,7 +12,7 @@ import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-
import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -36,8 +36,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
@InjectRepository(RowLevelPermissionPredicateEntity)
@@ -56,7 +56,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
applications,
roleTargets,
objectPermissions,
permissionFlags,
rolePermissionFlags,
fieldPermissions,
rowLevelPermissionPredicates,
rowLevelPermissionPredicateGroups,
@@ -80,7 +80,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
this.permissionFlagRepository.find({
this.rolePermissionFlagRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
@@ -105,7 +105,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
const [
roleTargetsByRoleId,
objectPermissionsByRoleId,
permissionFlagsByRoleId,
rolePermissionFlagsByRoleId,
fieldPermissionsByRoleId,
rowLevelPermissionPredicatesByRoleId,
rowLevelPermissionPredicateGroupsByRoleId,
@@ -120,7 +120,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
foreignKey: 'roleId',
},
{
entities: permissionFlags,
entities: rolePermissionFlags,
foreignKey: 'roleId',
},
{
@@ -149,7 +149,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
...roleEntity,
roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [],
objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [],
permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [],
rolePermissionFlags:
rolePermissionFlagsByRoleId.get(roleEntity.id) || [],
fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [],
rowLevelPermissionPredicates:
rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [],
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
type ObjectsPermissions,
@@ -16,7 +19,7 @@ import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/wo
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -41,8 +44,8 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
@InjectRepository(RowLevelPermissionPredicateEntity)
@@ -59,7 +62,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const [
roles,
objectPermissions,
permissionFlags,
rolePermissionFlags,
fieldPermissions,
rowLevelPermissionPredicates,
rowLevelPermissionPredicateGroups,
@@ -71,8 +74,9 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
this.objectPermissionRepository.find({
where: { workspaceId },
}),
this.permissionFlagRepository.find({
this.rolePermissionFlagRepository.find({
where: { workspaceId },
relations: ['permissionFlag'],
}),
this.fieldPermissionRepository.find({
where: { workspaceId },
@@ -91,9 +95,9 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
entities: objectPermissions,
foreignKey: 'roleId',
});
const permissionFlagsByRoleId =
regroupEntitiesByRelatedEntityId<'permissionFlag'>({
entities: permissionFlags,
const rolePermissionFlagsByRoleId =
regroupEntitiesByRelatedEntityId<'rolePermissionFlag'>({
entities: rolePermissionFlags,
foreignKey: 'roleId',
});
const fieldPermissionsByRoleId =
@@ -117,7 +121,8 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
for (const role of roles) {
const roleObjectPermissions =
objectPermissionsByRoleId.get(role.id) ?? [];
const rolePermissionFlags = permissionFlagsByRoleId.get(role.id) ?? [];
const roleRolePermissionFlags =
rolePermissionFlagsByRoleId.get(role.id) ?? [];
const roleFieldPermissions = fieldPermissionsByRoleId.get(role.id) ?? [];
const roleRowLevelPermissionPredicates =
rowLevelPermissionPredicatesByRoleId.get(role.id) ?? [];
@@ -150,7 +155,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const hasWorkflowsPermissions =
this.hasSettingsGatedObjectPermissions(
role,
rolePermissionFlags,
roleRolePermissionFlags,
PermissionFlagType.WORKFLOWS,
);
@@ -163,7 +168,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const hasWorkspaceMembersPermissions =
this.hasSettingsGatedObjectPermissions(
role,
rolePermissionFlags,
roleRolePermissionFlags,
PermissionFlagType.WORKSPACE_MEMBERS,
);
@@ -269,13 +274,18 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
private hasSettingsGatedObjectPermissions(
role: RoleEntity,
permissionFlags: PermissionFlagEntity[],
rolePermissionFlags: RolePermissionFlagEntity[],
permissionFlagType: PermissionFlagType,
): boolean {
const hasPermissionFromRole = role.canUpdateAllSettings;
const permissionFlagUniversalIdentifier =
SystemPermissionFlag[permissionFlagType];
const hasPermissionFromSettingPermissions = isDefined(
permissionFlags.find(
(permissionFlag) => permissionFlag.flag === permissionFlagType,
rolePermissionFlags.find(
(rolePermissionFlag) =>
(rolePermissionFlag.permissionFlag?.universalIdentifier ??
SystemPermissionFlag[rolePermissionFlag.flag]) ===
permissionFlagUniversalIdentifier,
),
);
@@ -0,0 +1,65 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
const buildRoleEntity = (overrides: Partial<RoleEntity> = {}): RoleEntity =>
({
id: 'role-id',
label: 'Role',
universalIdentifier: 'role-universal-identifier',
canUpdateAllSettings: false,
canAccessAllTools: false,
description: null,
icon: null,
isEditable: true,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
rolePermissionFlags: [],
...overrides,
}) as RoleEntity;
describe('fromRoleEntityToRoleDto', () => {
it('returns undefined permission flags when role permission flags are not loaded', () => {
const roleDto = fromRoleEntityToRoleDto(
buildRoleEntity({
rolePermissionFlags:
undefined as unknown as RoleEntity['rolePermissionFlags'],
}),
);
expect(roleDto.permissionFlags).toBeUndefined();
});
it('maps loaded role permission flags to the public permission flag DTO shape', () => {
const roleDto = fromRoleEntityToRoleDto(
buildRoleEntity({
rolePermissionFlags: [
{
id: 'role-permission-flag-id',
roleId: 'role-id',
permissionFlag: {
key: PermissionFlagType.WORKSPACE,
},
},
] as RoleEntity['rolePermissionFlags'],
}),
);
expect(roleDto.permissionFlags).toEqual([
{
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
},
]);
});
});
@@ -1,3 +1,5 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -19,7 +21,12 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
canBeAssignedToAgents: role.canBeAssignedToAgents,
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
roleTargets: role.roleTargets,
permissionFlags: role.permissionFlags,
permissionFlags: role.rolePermissionFlags?.map((rolePermissionFlag) => ({
id: rolePermissionFlag.id,
roleId: rolePermissionFlag.roleId,
flag: (rolePermissionFlag.permissionFlag?.key ??
rolePermissionFlag.flag) as PermissionFlagType,
})),
objectPermissions: role.objectPermissions,
fieldPermissions: role.fieldPermissions,
};
@@ -108,7 +108,9 @@ export class UserRoleService {
},
relations: {
role: {
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
},
},
});
@@ -35,7 +35,8 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
flatApplicationMaps: 'flat-maps:flatApplicationMaps',
flatRoleMaps: 'flat-maps:role',
flatRoleTargetMaps: 'flat-maps:role-target',
flatPermissionFlagMaps: 'flat-maps:permission-flag',
flatRolePermissionFlagMaps: 'flat-maps:role-permission-flag',
flatPermissionFlagMaps: 'flat-maps:permission-flag-catalog',
flatObjectPermissionMaps: 'flat-maps:object-permission',
flatFieldPermissionMaps: 'flat-maps:field-permission',
ORMEntityMetadatas: 'orm:entity-metadatas',
@@ -10,6 +10,7 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [
'viewGroup',
'view',
'navigationMenuItem',
'permissionFlag',
'role',
'agent',
'skill',
@@ -0,0 +1,41 @@
import { v4 } from 'uuid';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { STANDARD_PERMISSION_FLAG_DEFINITIONS } from 'src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
export const buildStandardFlatPermissionFlagMetadataMaps = ({
now,
workspaceId,
twentyStandardApplicationId,
}: {
now: string;
workspaceId: string;
twentyStandardApplicationId: string;
}): FlatEntityMaps<FlatPermissionFlag> => {
let flatPermissionFlagMaps = createEmptyFlatEntityMaps();
for (const permissionFlagDefinition of STANDARD_PERMISSION_FLAG_DEFINITIONS) {
flatPermissionFlagMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: {
id: v4(),
...permissionFlagDefinition,
workspaceId,
applicationId: twentyStandardApplicationId,
universalIdentifier: permissionFlagDefinition.universalIdentifier,
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
},
flatEntityMaps: flatPermissionFlagMaps,
});
}
return flatPermissionFlagMaps;
};
@@ -72,8 +72,8 @@ export const createStandardRoleFlatMetadata = ({
TWENTY_STANDARD_APPLICATION.universalIdentifier,
createdAt: now,
updatedAt: now,
permissionFlagIds: [],
permissionFlagUniversalIdentifiers: [],
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
@@ -15,6 +15,7 @@ import { buildStandardFlatObjectMetadataMaps } from 'src/engine/workspace-manage
import { buildStandardFlatPageLayoutTabMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/build-standard-flat-page-layout-tab-metadata-maps.util';
import { buildStandardFlatPageLayoutWidgetMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util';
import { buildStandardFlatPageLayoutMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout/build-standard-flat-page-layout-metadata-maps.util';
import { buildStandardFlatPermissionFlagMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/permission-flag/build-standard-flat-permission-flag-metadata-maps.util';
import { buildStandardFlatRoleMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/build-standard-flat-role-metadata-maps.util';
import { buildStandardFlatSkillMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/build-standard-flat-skill-metadata-maps.util';
import { buildStandardFlatViewFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/build-standard-flat-view-field-metadata-maps.util';
@@ -140,6 +141,12 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
dependencyFlatEntityMaps: undefined,
});
const flatPermissionFlagMaps = buildStandardFlatPermissionFlagMetadataMaps({
now,
workspaceId,
twentyStandardApplicationId,
});
const flatAgentMaps = buildStandardFlatAgentMetadataMaps({
now,
workspaceId,
@@ -213,6 +220,7 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatNavigationMenuItemMaps,
flatPermissionFlagMaps,
flatRoleMaps,
flatAgentMaps,
flatSkillMaps,
@@ -167,6 +167,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
case 'navigationMenuItem':
case 'frontComponent':
case 'viewSort':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
@@ -29,6 +29,7 @@ import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine
import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service';
import { WorkspaceMigrationPageLayoutActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/workspace-migration-page-layout-actions-builder.service';
import { WorkspaceMigrationPermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service';
import { WorkspaceMigrationRolePermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/workspace-migration-role-permission-flag-actions-builder.service';
import { WorkspaceMigrationRoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-target/workspace-migration-role-target-actions-builder.service';
import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/workspace-migration-role-actions-builder.service';
import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service';
@@ -58,6 +59,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationViewSortActionsBuilderService: WorkspaceMigrationViewSortActionsBuilderService,
private readonly workspaceMigrationFieldPermissionActionsBuilderService: WorkspaceMigrationFieldPermissionActionsBuilderService,
private readonly workspaceMigrationObjectPermissionActionsBuilderService: WorkspaceMigrationObjectPermissionActionsBuilderService,
private readonly workspaceMigrationRolePermissionFlagActionsBuilderService: WorkspaceMigrationRolePermissionFlagActionsBuilderService,
private readonly workspaceMigrationPermissionFlagActionsBuilderService: WorkspaceMigrationPermissionFlagActionsBuilderService,
private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService,
private readonly workspaceMigrationRoleTargetActionsBuilderService: WorkspaceMigrationRoleTargetActionsBuilderService,
@@ -158,6 +160,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatRoleMaps,
flatObjectPermissionMaps,
flatFieldPermissionMaps,
flatRolePermissionFlagMaps,
flatPermissionFlagMaps,
flatRoleTargetMaps,
flatAgentMaps,
@@ -558,6 +561,34 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatRolePermissionFlagMaps)) {
const {
from: fromFlatRolePermissionFlagMaps,
to: toFlatRolePermissionFlagMaps,
} = flatRolePermissionFlagMaps;
const rolePermissionFlagResult =
await this.workspaceMigrationRolePermissionFlagActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatRolePermissionFlagMaps,
to: toFlatRolePermissionFlagMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps,
workspaceId,
},
);
if (rolePermissionFlagResult.status === 'fail') {
orchestratorFailureReport.rolePermissionFlag.push(
...rolePermissionFlagResult.errors,
);
} else {
orchestratorActionsReport.rolePermissionFlag =
rolePermissionFlagResult.actions;
}
}
if (isDefined(flatPermissionFlagMaps)) {
const { from: fromFlatPermissionFlagMaps, to: toFlatPermissionFlagMaps } =
flatPermissionFlagMaps;
@@ -986,6 +1017,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
///
// Permission flags
...aggregatedOrchestratorActionsReport.rolePermissionFlag.delete,
...aggregatedOrchestratorActionsReport.rolePermissionFlag.create,
...aggregatedOrchestratorActionsReport.rolePermissionFlag.update,
///
// Permission flag definitions
...aggregatedOrchestratorActionsReport.permissionFlag.delete,
...aggregatedOrchestratorActionsReport.permissionFlag.create,
...aggregatedOrchestratorActionsReport.permissionFlag.update,
@@ -35,15 +35,18 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
"widgetUniversalIdentifiers",
],
"pageLayoutWidget": [],
"permissionFlag": [],
"permissionFlag": [
"rolePermissionFlagUniversalIdentifiers",
],
"role": [
"roleTargetUniversalIdentifiers",
"objectPermissionUniversalIdentifiers",
"permissionFlagUniversalIdentifiers",
"rolePermissionFlagUniversalIdentifiers",
"fieldPermissionUniversalIdentifiers",
"rowLevelPermissionPredicateUniversalIdentifiers",
"rowLevelPermissionPredicateGroupUniversalIdentifiers",
],
"rolePermissionFlag": [],
"roleTarget": [],
"rowLevelPermissionPredicate": [],
"rowLevelPermissionPredicateGroup": [
@@ -38,6 +38,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
},
commandMenuItem: {},
navigationMenuItem: {},
rolePermissionFlag: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
@@ -0,0 +1,7 @@
import { type RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
export type UniversalFlatRolePermissionFlag = UniversalFlatEntityFrom<
RolePermissionFlagEntity,
'rolePermissionFlag'
>;
@@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { type UniversalUpdatePermissionFlagAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { type UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { UniversalUpdatePermissionFlagAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import { FlatPermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@Injectable()
export class WorkspaceMigrationPermissionFlagActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
@@ -105,7 +105,7 @@ export class WorkspaceMigrationPermissionFlagActionsBuilderService extends Works
const { universalIdentifier, flatEntityUpdate } = args;
const updatePermissionFlagAction: UniversalUpdatePermissionFlagAction = {
const updateAction: UniversalUpdatePermissionFlagAction = {
type: 'update',
metadataName: 'permissionFlag',
universalIdentifier,
@@ -114,7 +114,7 @@ export class WorkspaceMigrationPermissionFlagActionsBuilderService extends Works
return {
status: 'success',
action: updatePermissionFlagAction,
action: updateAction,
};
}
}
@@ -0,0 +1,24 @@
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type';
import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type';
import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type';
import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type';
export type FlatCreateRolePermissionFlagAction =
BaseFlatCreateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type FlatUpdateRolePermissionFlagAction =
BaseFlatUpdateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type FlatDeleteRolePermissionFlagAction =
BaseFlatDeleteWorkspaceMigrationAction<'rolePermissionFlag'>;
export type UniversalCreateRolePermissionFlagAction =
BaseUniversalCreateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type UniversalUpdateRolePermissionFlagAction =
BaseUniversalUpdateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type UniversalDeleteRolePermissionFlagAction =
BaseUniversalDeleteWorkspaceMigrationAction<'rolePermissionFlag'>;
@@ -0,0 +1,122 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { UniversalUpdateRolePermissionFlagAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import { FlatRolePermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-role-permission-flag-validator.service';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@Injectable()
export class WorkspaceMigrationRolePermissionFlagActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.rolePermissionFlag
> {
constructor(
private readonly flatRolePermissionFlagValidatorService: FlatRolePermissionFlagValidatorService,
) {
super(ALL_METADATA_NAME.rolePermissionFlag);
}
protected validateFlatEntityCreation(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.rolePermissionFlag,
'create'
> {
const validationResult =
this.flatRolePermissionFlagValidatorService.validateFlatRolePermissionFlagCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRolePermissionFlagToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'rolePermissionFlag',
flatEntity: flatRolePermissionFlagToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.rolePermissionFlag,
'delete'
> {
const validationResult =
this.flatRolePermissionFlagValidatorService.validateFlatRolePermissionFlagDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRolePermissionFlagToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'rolePermissionFlag',
universalIdentifier:
flatRolePermissionFlagToValidate.universalIdentifier,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.rolePermissionFlag,
'update'
> {
const validationResult =
this.flatRolePermissionFlagValidatorService.validateFlatRolePermissionFlagUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { universalIdentifier, flatEntityUpdate } = args;
const updateRolePermissionFlagAction: UniversalUpdateRolePermissionFlagAction =
{
type: 'update',
metadataName: 'rolePermissionFlag',
universalIdentifier,
update: flatEntityUpdate,
};
return {
status: 'success',
action: updateRolePermissionFlagAction,
};
}
}
@@ -0,0 +1,381 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { PermissionFlagType } from 'twenty-shared/constants';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
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';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { PermissionFlagExceptionCode } from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { FlatPermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service';
const buildFlatDefinition = (
overrides: Partial<FlatPermissionFlag> = {},
): FlatPermissionFlag =>
({
id: '00000000-0000-0000-0000-000000000001',
universalIdentifier: '00000000-0000-0000-0000-000000000001',
key: 'TEST_FLAG',
label: 'Test Flag',
description: 'A flag for tests',
icon: 'IconTest',
permissionType: 'tool',
workspaceId: 'workspace-id',
applicationId: '00000000-0000-0000-0000-000000000aaa',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000aaa',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
}) as unknown as FlatPermissionFlag;
const buildEmptyMaps = (): FlatEntityMaps<FlatPermissionFlag> =>
createEmptyFlatEntityMaps() as unknown as FlatEntityMaps<FlatPermissionFlag>;
const buildEmptyRolePermissionFlagMaps =
(): FlatEntityMaps<FlatRolePermissionFlag> =>
createEmptyFlatEntityMaps() as unknown as FlatEntityMaps<FlatRolePermissionFlag>;
const buildFlatRolePermissionFlag = (
overrides: Partial<FlatRolePermissionFlag> = {},
): FlatRolePermissionFlag =>
({
id: '00000000-0000-0000-0000-000000000101',
universalIdentifier: '00000000-0000-0000-0000-000000000101',
permissionFlagId: '00000000-0000-0000-0000-000000000001',
permissionFlagUniversalIdentifier: '00000000-0000-0000-0000-000000000001',
roleUniversalIdentifier: '00000000-0000-0000-0000-000000000201',
workspaceId: 'workspace-id',
applicationId: '00000000-0000-0000-0000-000000000aaa',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000aaa',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
}) as unknown as FlatRolePermissionFlag;
const buildArgs = (
flatEntityToValidate: FlatPermissionFlag,
optimisticMaps: FlatEntityMaps<FlatPermissionFlag> = buildEmptyMaps(),
) =>
({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
}) as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagCreation']
>[0];
describe('FlatPermissionFlagValidatorService', () => {
let service: FlatPermissionFlagValidatorService;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [FlatPermissionFlagValidatorService],
}).compile();
service = moduleRef.get(FlatPermissionFlagValidatorService);
});
describe('validateFlatPermissionFlagCreation', () => {
it('passes for a valid definition', () => {
const result = service.validateFlatPermissionFlagCreation(
buildArgs(buildFlatDefinition()),
);
expect(result.errors).toHaveLength(0);
});
it('rejects an empty key', () => {
const result = service.validateFlatPermissionFlagCreation(
buildArgs(buildFlatDefinition({ key: '' })),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_KEY,
]);
});
it('rejects an unknown permission type', () => {
const result = service.validateFlatPermissionFlagCreation(
buildArgs(
buildFlatDefinition({
permissionType: 'invalid' as PermissionFlagPermissionType,
}),
),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
]);
});
it('rejects a duplicate key in the same workspace', () => {
const existing = buildFlatDefinition({
universalIdentifier: '00000000-0000-0000-0000-000000000999',
key: 'EXISTING_KEY',
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagCreation(
buildArgs(buildFlatDefinition({ key: 'EXISTING_KEY' }), optimisticMaps),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
]);
});
it('rejects a duplicate universal identifier', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagCreation(
buildArgs(
buildFlatDefinition({
id: '00000000-0000-0000-0000-000000000002',
key: 'ANOTHER_TEST_FLAG',
}),
optimisticMaps,
),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
]);
});
});
describe('validateFlatPermissionFlagUpdate', () => {
it('passes for a valid update', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { label: 'Updated Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors).toHaveLength(0);
});
it('rejects updating the immutable key field', () => {
const existing = buildFlatDefinition({ key: 'ORIGINAL_KEY' });
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { key: 'NEW_KEY' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_KEY_IMMUTABLE,
]);
});
it('returns not-found if no existing definition matches the universalIdentifier', () => {
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: '00000000-0000-0000-0000-deadbeefdead',
flatEntityUpdate: { label: 'New Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: buildEmptyMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
]);
});
it('rejects updating a standard definition from a custom application', () => {
const existing = buildFlatDefinition({
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { label: 'Updated Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
'00000000-0000-0000-0000-000000000aaa',
},
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
]);
});
it('rejects updating to an unknown permission type', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: {
permissionType: 'invalid' as PermissionFlagPermissionType,
},
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
]);
});
});
describe('validateFlatPermissionFlagDeletion', () => {
it('passes for a valid deletion', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: existing,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
flatRolePermissionFlagMaps: buildEmptyRolePermissionFlagMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors).toHaveLength(0);
});
it('returns not-found if no existing definition matches the universalIdentifier', () => {
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: buildFlatDefinition({
universalIdentifier: '00000000-0000-0000-0000-deadbeefdead',
}),
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: buildEmptyMaps(),
flatRolePermissionFlagMaps: buildEmptyRolePermissionFlagMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
]);
});
it('rejects deleting a standard definition from a custom application', () => {
const existing = buildFlatDefinition({
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: existing,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
flatRolePermissionFlagMaps: buildEmptyRolePermissionFlagMaps(),
},
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
'00000000-0000-0000-0000-000000000aaa',
},
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
]);
});
it('rejects deleting a definition while roles still grant its key', () => {
const existing = buildFlatDefinition({
key: PermissionFlagType.WORKSPACE,
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const rolePermissionFlagMaps = buildEmptyRolePermissionFlagMaps();
const rolePermissionFlag = buildFlatRolePermissionFlag({
permissionFlagId: existing.id,
permissionFlagUniversalIdentifier: existing.universalIdentifier,
});
rolePermissionFlagMaps.byUniversalIdentifier[
rolePermissionFlag.universalIdentifier
] = rolePermissionFlag;
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: existing,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
flatRolePermissionFlagMaps: rolePermissionFlagMaps,
},
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
'00000000-0000-0000-0000-000000000aaa',
},
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE,
]);
});
});
// ALL_METADATA_NAME is referenced as a sanity check the metadata name exists
it('uses the registered ALL_METADATA_NAME entry', () => {
expect(ALL_METADATA_NAME.permissionFlag).toBe('permissionFlag');
});
});
@@ -1,17 +1,19 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isNonEmptyString } from '@sniptt/guards';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { PERMISSION_FLAG_PERMISSION_TYPES } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { PermissionFlagExceptionCode } from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { validateRoleBelongsToCallerApplication } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/utils/validate-role-belongs-to-caller-application.util';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
@Injectable()
export class FlatPermissionFlagValidatorService {
@@ -19,17 +21,14 @@ export class FlatPermissionFlagValidatorService {
flatEntityToValidate: flatPermissionFlagToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.permissionFlag
>): FailedFlatEntityValidation<'permissionFlag', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: flatPermissionFlagToValidate.universalIdentifier,
roleUniversalIdentifier:
flatPermissionFlagToValidate.roleUniversalIdentifier,
key: flatPermissionFlagToValidate.key,
},
metadataName: 'permissionFlag',
type: 'create',
@@ -42,71 +41,47 @@ export class FlatPermissionFlagValidatorService {
if (isDefined(existingByUniversalId)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag with universal identifier ${flatPermissionFlagToValidate.universalIdentifier} already exists`,
userFriendlyMessage: msg`Permission flag already exists`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
message: t`Permission flag definition with universal identifier ${flatPermissionFlagToValidate.universalIdentifier} already exists`,
userFriendlyMessage: msg`Permission flag definition already exists`,
});
}
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatPermissionFlagToValidate.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
if (!isNonEmptyString(flatPermissionFlagToValidate.key)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
const isValidFlag =
isDefined(flatPermissionFlagToValidate.flag) &&
Object.values(PermissionFlagType).includes(
flatPermissionFlagToValidate.flag,
);
if (!isValidFlag) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Invalid permission flag value`,
userFriendlyMessage: msg`Invalid permission setting`,
code: PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_KEY,
message: t`Permission flag definition key is required`,
userFriendlyMessage: msg`Key is required`,
});
}
const duplicateForSameRole = Object.values(
const duplicateKey = Object.values(
optimisticFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
flatPermissionFlagToValidate.roleUniversalIdentifier &&
pf.flag === flatPermissionFlagToValidate.flag &&
pf.universalIdentifier !==
(definition) =>
isDefined(definition) &&
definition.key === flatPermissionFlagToValidate.key &&
definition.universalIdentifier !==
flatPermissionFlagToValidate.universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
if (duplicateKey.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
message: t`Permission flag definition with key ${flatPermissionFlagToValidate.key} already exists in this workspace`,
userFriendlyMessage: msg`A permission flag with this key already exists`,
});
}
if (
!PERMISSION_FLAG_PERMISSION_TYPES.includes(
flatPermissionFlagToValidate.permissionType,
)
) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
message: t`Permission flag definition permission type must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid permission type`,
});
}
@@ -118,17 +93,11 @@ export class FlatPermissionFlagValidatorService {
flatEntityUpdate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.permissionFlag
>): FailedFlatEntityValidation<'permissionFlag', 'update'> {
const existingFlatPermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatPermissionFlagMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
@@ -137,79 +106,56 @@ export class FlatPermissionFlagValidatorService {
type: 'update',
});
if (!isDefined(existingFlatPermissionFlag)) {
const existing = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatPermissionFlagMaps,
});
if (!isDefined(existing)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to update not found`,
userFriendlyMessage: msg`Permission flag not found`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
message: t`Permission flag definition to update not found`,
userFriendlyMessage: msg`Permission flag definition not found`,
});
return validationResult;
}
const updatedFlatPermissionFlag = {
...existingFlatPermissionFlag,
...flatEntityUpdate,
};
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier: updatedFlatPermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
if (
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp({
universalIdentifier: existing.universalIdentifier,
applicationUniversalIdentifier: existing.applicationUniversalIdentifier,
})
) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
message: t`Cannot update standard permission flag definition`,
userFriendlyMessage: msg`Cannot update standard permission flag definition`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
if (isDefined(flatEntityUpdate.flag)) {
const isValidFlag = Object.values(PermissionFlagType).includes(
flatEntityUpdate.flag as PermissionFlagType,
);
if (!isValidFlag) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Invalid permission flag value`,
userFriendlyMessage: msg`Invalid permission setting`,
});
}
}
const duplicateForSameRole = Object.values(
optimisticFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
updatedFlatPermissionFlag.roleUniversalIdentifier &&
pf.flag === updatedFlatPermissionFlag.flag &&
pf.universalIdentifier !== universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
if (
isDefined(flatEntityUpdate.key) &&
flatEntityUpdate.key !== existing.key
) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_KEY_IMMUTABLE,
message: t`Permission flag definition key cannot be changed after creation`,
userFriendlyMessage: msg`Key cannot be changed`,
});
}
if (
isDefined(flatEntityUpdate.permissionType) &&
!PERMISSION_FLAG_PERMISSION_TYPES.includes(
flatEntityUpdate.permissionType,
)
) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
message: t`Permission flag definition permission type must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid permission type`,
});
}
@@ -220,8 +166,9 @@ export class FlatPermissionFlagValidatorService {
flatEntityToValidate: { universalIdentifier },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps,
flatRoleMaps,
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
},
buildOptions,
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.permissionFlag
>): FailedFlatEntityValidation<'permissionFlag', 'delete'> {
@@ -233,30 +180,50 @@ export class FlatPermissionFlagValidatorService {
type: 'delete',
});
const existingFlatPermissionFlag = findFlatEntityByUniversalIdentifier({
const existing = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatPermissionFlagMaps,
});
if (!isDefined(existingFlatPermissionFlag)) {
if (!isDefined(existing)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to delete not found`,
userFriendlyMessage: msg`Permission flag not found`,
});
} else {
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier: existingFlatPermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
message: t`Permission flag definition to delete not found`,
userFriendlyMessage: msg`Permission flag definition not found`,
});
if (isDefined(referencedRole) && !referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
return validationResult;
}
if (
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp({
universalIdentifier: existing.universalIdentifier,
applicationUniversalIdentifier: existing.applicationUniversalIdentifier,
})
) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
message: t`Cannot delete standard permission flag definition`,
userFriendlyMessage: msg`Cannot delete standard permission flag definition`,
});
}
const isPermissionFlagInUse = Object.values(
optimisticFlatRolePermissionFlagMaps.byUniversalIdentifier,
).some(
(rolePermissionFlag) =>
isDefined(rolePermissionFlag) &&
rolePermissionFlag.permissionFlagUniversalIdentifier ===
existing.universalIdentifier,
);
if (isPermissionFlagInUse) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE,
message: t`Permission flag definition with key ${existing.key} is still assigned to a role`,
userFriendlyMessage: msg`Remove this permission from all roles before deleting it`,
});
}
return validationResult;
@@ -0,0 +1,281 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { validateRoleBelongsToCallerApplication } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/utils/validate-role-belongs-to-caller-application.util';
@Injectable()
export class FlatRolePermissionFlagValidatorService {
validateFlatRolePermissionFlagCreation({
flatEntityToValidate: flatRolePermissionFlagToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps,
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>): FailedFlatEntityValidation<'rolePermissionFlag', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier:
flatRolePermissionFlagToValidate.universalIdentifier,
permissionFlagUniversalIdentifier:
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier,
roleUniversalIdentifier:
flatRolePermissionFlagToValidate.roleUniversalIdentifier,
},
metadataName: 'rolePermissionFlag',
type: 'create',
});
const existingByUniversalId = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatRolePermissionFlagToValidate.universalIdentifier,
flatEntityMaps: optimisticFlatRolePermissionFlagMaps,
});
if (isDefined(existingByUniversalId)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Role permission flag with universal identifier ${flatRolePermissionFlagToValidate.universalIdentifier} already exists`,
userFriendlyMessage: msg`Role permission flag already exists`,
});
}
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatRolePermissionFlagToValidate.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
if (
isDefined(
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier,
)
) {
const referencedPermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(referencedPermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag not found`,
userFriendlyMessage: msg`Invalid permission setting`,
});
}
}
const duplicateForSameRole = Object.values(
optimisticFlatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
flatRolePermissionFlagToValidate.roleUniversalIdentifier &&
pf.permissionFlagUniversalIdentifier ===
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier &&
pf.universalIdentifier !==
flatRolePermissionFlagToValidate.universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
});
}
return validationResult;
}
validateFlatRolePermissionFlagUpdate({
universalIdentifier,
flatEntityUpdate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps,
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>): FailedFlatEntityValidation<'rolePermissionFlag', 'update'> {
const existingFlatRolePermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatRolePermissionFlagMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'rolePermissionFlag',
type: 'update',
});
if (!isDefined(existingFlatRolePermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to update not found`,
userFriendlyMessage: msg`Permission flag not found`,
});
return validationResult;
}
const updatedFlatRolePermissionFlag = {
...existingFlatRolePermissionFlag,
...flatEntityUpdate,
};
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
updatedFlatRolePermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
if (isDefined(flatEntityUpdate.permissionFlagUniversalIdentifier)) {
const referencedPermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatEntityUpdate.permissionFlagUniversalIdentifier as string,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(referencedPermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag not found`,
userFriendlyMessage: msg`Invalid permission setting`,
});
}
}
const duplicateForSameRole = Object.values(
optimisticFlatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
updatedFlatRolePermissionFlag.roleUniversalIdentifier &&
pf.permissionFlagUniversalIdentifier ===
updatedFlatRolePermissionFlag.permissionFlagUniversalIdentifier &&
pf.universalIdentifier !== universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
});
}
return validationResult;
}
validateFlatRolePermissionFlagDeletion({
flatEntityToValidate: { universalIdentifier },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
flatRoleMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>): FailedFlatEntityValidation<'rolePermissionFlag', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'rolePermissionFlag',
type: 'delete',
});
const existingFlatRolePermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatRolePermissionFlagMaps,
});
if (!isDefined(existingFlatRolePermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to delete not found`,
userFriendlyMessage: msg`Permission flag not found`,
});
} else {
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
existingFlatRolePermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (isDefined(referencedRole) && !referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
return validationResult;
}
}

Some files were not shown because too many files have changed in this diff Show More