[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:
+22
-13
@@ -1,7 +1,10 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-shared/constants';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
@@ -60,7 +63,7 @@ describe('PermissionsService', () => {
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
permissionFlags: [],
|
||||
rolePermissionFlags: [],
|
||||
workspaceId: 'test-workspace-id',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -133,7 +136,7 @@ describe('PermissionsService', () => {
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
permissionFlags: [],
|
||||
rolePermissionFlags: [],
|
||||
workspaceId: 'test-workspace-id',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -184,7 +187,7 @@ describe('PermissionsService', () => {
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
permissionFlags: [],
|
||||
rolePermissionFlags: [],
|
||||
roleTargets: [],
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
@@ -254,7 +257,7 @@ describe('PermissionsService', () => {
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
permissionFlags: [],
|
||||
rolePermissionFlags: [],
|
||||
roleTargets: [],
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
@@ -292,8 +295,8 @@ describe('PermissionsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Granular permissions with permissionFlags', () => {
|
||||
it('should grant specific tool permission when included in permissionFlags even if canAccessAllTools is false', () => {
|
||||
describe('Granular permissions with rolePermissionFlags', () => {
|
||||
it('should grant specific tool permission when included in rolePermissionFlags even if canAccessAllTools is false', () => {
|
||||
const roleWithSpecificPermission: Partial<RoleEntity> = {
|
||||
id: 'test-role-id',
|
||||
label: 'Test Role',
|
||||
@@ -308,10 +311,13 @@ describe('PermissionsService', () => {
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
permissionFlags: [
|
||||
rolePermissionFlags: [
|
||||
{
|
||||
id: 'permission-1',
|
||||
flag: PermissionFlagType.UPLOAD_FILE,
|
||||
permissionFlag: {
|
||||
key: PermissionFlagType.UPLOAD_FILE,
|
||||
universalIdentifier: SystemPermissionFlag.UPLOAD_FILE,
|
||||
},
|
||||
roleId: 'test-role-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
createdAt: new Date(),
|
||||
@@ -338,7 +344,7 @@ describe('PermissionsService', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should grant specific settings permission when included in permissionFlags even if canUpdateAllSettings is false', () => {
|
||||
it('should grant specific settings permission when included in rolePermissionFlags even if canUpdateAllSettings is false', () => {
|
||||
const roleWithSpecificPermission: Partial<RoleEntity> = {
|
||||
id: 'test-role-id',
|
||||
label: 'Test Role',
|
||||
@@ -353,10 +359,13 @@ describe('PermissionsService', () => {
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
permissionFlags: [
|
||||
rolePermissionFlags: [
|
||||
{
|
||||
id: 'permission-1',
|
||||
flag: PermissionFlagType.ROLES,
|
||||
permissionFlag: {
|
||||
key: PermissionFlagType.ROLES,
|
||||
universalIdentifier: SystemPermissionFlag.ROLES,
|
||||
},
|
||||
roleId: 'test-role-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
createdAt: new Date(),
|
||||
@@ -400,7 +409,7 @@ describe('PermissionsService', () => {
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
permissionFlags: [],
|
||||
rolePermissionFlags: [],
|
||||
roleTargets: [],
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
|
||||
+32
-17
@@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
@@ -66,7 +69,7 @@ export class PermissionsService {
|
||||
|
||||
const defaultSettingsPermissions =
|
||||
this.getDefaultUserWorkspacePermissions().permissionFlags;
|
||||
const permissionFlags = Object.keys(PermissionFlagType).reduce(
|
||||
const permissionFlags = Object.values(PermissionFlagType).reduce(
|
||||
(acc, feature) => {
|
||||
const hasBasePermission = this.isToolPermission(feature)
|
||||
? roleOfUserWorkspace.canAccessAllTools
|
||||
@@ -76,9 +79,7 @@ export class PermissionsService {
|
||||
...acc,
|
||||
[feature]:
|
||||
hasBasePermission ||
|
||||
roleOfUserWorkspace.permissionFlags.some(
|
||||
(permissionFlag) => permissionFlag.flag === feature,
|
||||
),
|
||||
this.roleHasPermissionFlag(roleOfUserWorkspace, feature),
|
||||
};
|
||||
},
|
||||
defaultSettingsPermissions,
|
||||
@@ -150,7 +151,10 @@ export class PermissionsService {
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id: roleId, workspaceId },
|
||||
relations: ['permissionFlags'],
|
||||
relations: [
|
||||
'rolePermissionFlags',
|
||||
'rolePermissionFlags.permissionFlag',
|
||||
],
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
@@ -203,7 +207,10 @@ export class PermissionsService {
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id: applicationRoleId, workspaceId },
|
||||
relations: ['permissionFlags'],
|
||||
relations: [
|
||||
'rolePermissionFlags',
|
||||
'rolePermissionFlags.permissionFlag',
|
||||
],
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
@@ -240,10 +247,22 @@ export class PermissionsService {
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissionFlags = role.permissionFlags ?? [];
|
||||
return this.roleHasPermissionFlag(role, setting);
|
||||
}
|
||||
|
||||
return permissionFlags.some(
|
||||
(permissionFlag) => permissionFlag.flag === setting,
|
||||
private roleHasPermissionFlag(
|
||||
role: RoleEntity,
|
||||
flag: PermissionFlagType,
|
||||
): boolean {
|
||||
const rolePermissionFlags = role.rolePermissionFlags ?? [];
|
||||
|
||||
const permissionFlagUniversalIdentifier = SystemPermissionFlag[flag];
|
||||
|
||||
return rolePermissionFlags.some(
|
||||
(rolePermissionFlag) =>
|
||||
(rolePermissionFlag.permissionFlag?.universalIdentifier ??
|
||||
SystemPermissionFlag[rolePermissionFlag.flag]) ===
|
||||
permissionFlagUniversalIdentifier,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -292,7 +311,7 @@ export class PermissionsService {
|
||||
const result = await this.getRolesFromPermissionConfig(
|
||||
rolePermissionConfig,
|
||||
workspaceId,
|
||||
['permissionFlags'],
|
||||
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
|
||||
);
|
||||
|
||||
if (result === null) {
|
||||
@@ -318,7 +337,7 @@ export class PermissionsService {
|
||||
const result = await this.getRolesFromPermissionConfig(
|
||||
rolePermissionConfig,
|
||||
workspaceId,
|
||||
['permissionFlags'],
|
||||
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
|
||||
);
|
||||
|
||||
if (result === null) {
|
||||
@@ -332,11 +351,7 @@ export class PermissionsService {
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissionFlags = role.permissionFlags ?? [];
|
||||
|
||||
return permissionFlags.some(
|
||||
(permissionFlag) => permissionFlag.flag === flag,
|
||||
);
|
||||
return this.roleHasPermissionFlag(role, flag);
|
||||
};
|
||||
|
||||
return useIntersection
|
||||
|
||||
Reference in New Issue
Block a user