feat: multi role permission intersection (#15150)
Implements permission intersection (AND logic) to prevent permission escalation when agents act on behalf of users. ### Changes: - **Permission Intersection**: Operations requiring both user AND agent permissions - **RoleContext Type**: Unified type supporting single `roleId` or multiple `roleIds` for intersection - **CRUD Services**: Updated to accept `roleContext` for granular permission control - **Agent Integration**: Chat agents now use user + agent role intersection for all operations - **ORM Layer**: Enhanced `getRepository` to support multi-role permission checks ### Related: - Part 2 of ["Acting on behalf of user" concept PR](https://github.com/twentyhq/twenty/pull/15103) [Closes #1661](https://github.com/twentyhq/core-team-issues/issues/1661) --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { type Entity } from '@microsoft/microsoft-graph-types';
|
||||
import { isDefined } from 'class-validator';
|
||||
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import {
|
||||
DataSource,
|
||||
type DataSourceOptions,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { type WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
type CreateQueryBuilderOptions = {
|
||||
calledByWorkspaceEntityManager?: boolean;
|
||||
@@ -34,7 +35,7 @@ export class WorkspaceDataSource extends DataSource {
|
||||
featureFlagMapVersion: string;
|
||||
featureFlagMap: FeatureFlagMap;
|
||||
rolesPermissionsVersion: string;
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated;
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleId;
|
||||
dataSourceWithOverridenCreateQueryBuilder: WorkspaceDataSource;
|
||||
isPoolSharingEnabled: boolean;
|
||||
|
||||
@@ -44,7 +45,7 @@ export class WorkspaceDataSource extends DataSource {
|
||||
featureFlagMapVersion: string,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
rolesPermissionsVersion: string,
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated,
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleId,
|
||||
isPoolSharingEnabled: boolean,
|
||||
) {
|
||||
super(options);
|
||||
@@ -60,31 +61,10 @@ export class WorkspaceDataSource extends DataSource {
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
shouldBypassPermissionChecks = false,
|
||||
roleId?: string,
|
||||
permissionOptions?: RolePermissionConfig,
|
||||
authContext?: AuthContext,
|
||||
): WorkspaceRepository<Entity> {
|
||||
if (shouldBypassPermissionChecks === true) {
|
||||
return this.manager.getRepository(
|
||||
target,
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
if (roleId) {
|
||||
return this.manager.getRepository(
|
||||
target,
|
||||
{
|
||||
roleId,
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
return this.manager.getRepository(target, undefined, authContext);
|
||||
return this.manager.getRepository(target, permissionOptions, authContext);
|
||||
}
|
||||
|
||||
override createEntityManager(
|
||||
@@ -238,9 +218,7 @@ export class WorkspaceDataSource extends DataSource {
|
||||
this.rolesPermissionsVersion = rolesPermissionsVersion;
|
||||
}
|
||||
|
||||
setRolesPermissions(
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated,
|
||||
) {
|
||||
setRolesPermissions(permissionsPerRoleId: ObjectsPermissionsByRoleId) {
|
||||
this.permissionsPerRoleId = permissionsPerRoleId;
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor';
|
||||
import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer';
|
||||
@@ -75,7 +75,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
let mockDataSource: WorkspaceDataSource;
|
||||
let mockPermissionOptions: {
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -184,10 +184,10 @@ describe('WorkspaceEntityManager', () => {
|
||||
shouldBypassPermissionChecks: false,
|
||||
objectRecordsPermissions: {
|
||||
'test-entity': {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
|
||||
+54
-22
@@ -1,5 +1,8 @@
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import {
|
||||
type ObjectsPermissions,
|
||||
type ObjectsPermissionsByRoleId,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type DeleteResult,
|
||||
@@ -53,14 +56,16 @@ import {
|
||||
} from 'src/engine/twenty-orm/repository/permissions.utils';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
type PermissionOptions = {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
};
|
||||
|
||||
export class WorkspaceEntityManager extends EntityManager {
|
||||
@@ -83,35 +88,62 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
return this.connection.featureFlagMap;
|
||||
}
|
||||
|
||||
private getPermissionsForRole(
|
||||
roleId: string,
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleId,
|
||||
): ObjectsPermissions {
|
||||
if (!isDefined(permissionsPerRoleId?.[roleId])) {
|
||||
throw new PermissionsException(
|
||||
`No permissions found for role in datasource (roleId: ${roleId})`,
|
||||
PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
return permissionsPerRoleId[roleId];
|
||||
}
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
permissionOptions?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
},
|
||||
rolePermissionConfig?: RolePermissionConfig,
|
||||
authContext?: AuthContext,
|
||||
): WorkspaceRepository<Entity> {
|
||||
const dataSource = this.connection;
|
||||
|
||||
let objectPermissions = {};
|
||||
let shouldBypassPermissionChecks = false;
|
||||
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
|
||||
|
||||
if (permissionOptions?.roleId) {
|
||||
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
|
||||
if (
|
||||
rolePermissionConfig &&
|
||||
'shouldBypassPermissionChecks' in rolePermissionConfig
|
||||
) {
|
||||
shouldBypassPermissionChecks =
|
||||
rolePermissionConfig.shouldBypassPermissionChecks;
|
||||
}
|
||||
|
||||
if (!isDefined(objectPermissionsByRoleId?.[permissionOptions.roleId])) {
|
||||
throw new PermissionsException(
|
||||
`No permissions found for role in datasource (missing ${
|
||||
!isDefined(objectPermissionsByRoleId)
|
||||
? 'objectPermissionsByRoleId object'
|
||||
: `roleId in objectPermissionsByRoleId object (${permissionOptions.roleId})`
|
||||
})`,
|
||||
PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE,
|
||||
if (rolePermissionConfig && 'unionOf' in rolePermissionConfig) {
|
||||
if (rolePermissionConfig.unionOf.length === 1) {
|
||||
objectPermissions = this.getPermissionsForRole(
|
||||
rolePermissionConfig.unionOf[0],
|
||||
objectPermissionsByRoleId,
|
||||
);
|
||||
} else {
|
||||
objectPermissions = objectPermissionsByRoleId[permissionOptions.roleId];
|
||||
// TODO: Implement union logic for combining permissions across multiple roles
|
||||
throw new Error(
|
||||
'Union permission logic for multiple roles not yet implemented',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (rolePermissionConfig && 'intersectionOf' in rolePermissionConfig) {
|
||||
const allRolePermissions = rolePermissionConfig.intersectionOf.map(
|
||||
(roleId: string) =>
|
||||
this.getPermissionsForRole(roleId, objectPermissionsByRoleId),
|
||||
);
|
||||
|
||||
objectPermissions = computePermissionIntersection(allRolePermissions);
|
||||
}
|
||||
|
||||
const newRepository = new WorkspaceRepository<Entity>(
|
||||
this.internalContext,
|
||||
target,
|
||||
@@ -119,7 +151,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
dataSource.featureFlagMap,
|
||||
this.queryRunner,
|
||||
objectPermissions,
|
||||
permissionOptions?.shouldBypassPermissionChecks,
|
||||
shouldBypassPermissionChecks,
|
||||
authContext,
|
||||
);
|
||||
|
||||
@@ -132,7 +164,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
queryRunner?: QueryRunner,
|
||||
options: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
} = {
|
||||
shouldBypassPermissionChecks: false,
|
||||
objectRecordsPermissions: {},
|
||||
@@ -200,7 +232,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
|
||||
permissionOptions?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
},
|
||||
selectedColumns: string[] | '*' = '*',
|
||||
): Promise<InsertResult> {
|
||||
@@ -389,7 +421,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
operationType: OperationType;
|
||||
permissionOptions?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
};
|
||||
selectedColumns: string[];
|
||||
updatedColumns?: string[];
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { EntitySchema, Repository } from 'typeorm';
|
||||
|
||||
@@ -55,7 +55,7 @@ export class WorkspaceDatasourceFactory {
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly getFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
|
||||
string,
|
||||
ObjectsPermissionsByRoleIdDeprecated
|
||||
ObjectsPermissionsByRoleId
|
||||
>,
|
||||
) {}
|
||||
|
||||
@@ -239,7 +239,7 @@ export class WorkspaceDatasourceFactory {
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<CacheResult<string, ObjectsPermissionsByRoleIdDeprecated>> {
|
||||
}): Promise<CacheResult<string, ObjectsPermissionsByRoleId>> {
|
||||
return this.getFromCacheWithRecomputeService.getFromCacheWithRecompute({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
@@ -292,7 +292,7 @@ export class WorkspaceDatasourceFactory {
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
cachedRolesPermissionsVersion: string;
|
||||
cachedRolesPermissions: ObjectsPermissionsByRoleIdDeprecated;
|
||||
cachedRolesPermissions: ObjectsPermissionsByRoleId;
|
||||
}): Promise<void> {
|
||||
this.updateWorkspaceDataSourceIfNeeded({
|
||||
workspaceDataSource,
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type DeepPartial,
|
||||
type FindManyOptions,
|
||||
@@ -20,7 +20,7 @@ describe('WorkspaceRepository', () => {
|
||||
let mockEntityManager: jest.Mocked<WorkspaceEntityManager>;
|
||||
let mockInternalContext: WorkspaceInternalContext;
|
||||
let mockFeatureFlagMap: FeatureFlagMap;
|
||||
let mockObjectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
let mockObjectRecordsPermissions: ObjectsPermissions;
|
||||
let mockQueryRunner: QueryRunner;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,10 +71,10 @@ describe('WorkspaceRepository', () => {
|
||||
);
|
||||
mockObjectRecordsPermissions = {
|
||||
'test-entity': {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import {
|
||||
type ObjectsPermissionsDeprecated,
|
||||
type ObjectsPermissions,
|
||||
type RestrictedFieldsPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -38,7 +38,7 @@ export type OperationType =
|
||||
type ValidateOperationIsPermittedOrThrowArgs = {
|
||||
entityName: string;
|
||||
operationType: OperationType;
|
||||
objectsPermissions: ObjectsPermissionsDeprecated;
|
||||
objectsPermissions: ObjectsPermissions;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
selectedColumns: string[] | '*';
|
||||
allFieldsSelected: boolean;
|
||||
@@ -86,7 +86,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
|
||||
switch (operationType) {
|
||||
case 'select':
|
||||
if (!permissionsForEntity?.canRead) {
|
||||
if (!permissionsForEntity?.canReadObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -102,7 +102,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
break;
|
||||
case 'insert':
|
||||
case 'update':
|
||||
if (!permissionsForEntity?.canUpdate) {
|
||||
if (!permissionsForEntity?.canUpdateObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -124,7 +124,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (!permissionsForEntity?.canDestroy) {
|
||||
if (!permissionsForEntity?.canDestroyObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -139,7 +139,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
break;
|
||||
case 'restore':
|
||||
case 'soft-delete':
|
||||
if (!permissionsForEntity?.canSoftDelete) {
|
||||
if (!permissionsForEntity?.canSoftDeleteObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -166,7 +166,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
|
||||
type ValidateQueryIsPermittedOrThrowArgs = {
|
||||
expressionMap: QueryExpressionMap;
|
||||
objectsPermissions: ObjectsPermissionsDeprecated;
|
||||
objectsPermissions: ObjectsPermissions;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
};
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
DeleteQueryBuilder,
|
||||
type DeleteResult,
|
||||
@@ -25,21 +25,21 @@ import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repo
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { applyTableAliasOnWhereCondition } from 'src/engine/twenty-orm/utils/apply-table-alias-on-where-condition';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
|
||||
export class WorkspaceDeleteQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends DeleteQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
constructor(
|
||||
queryBuilder: DeleteQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type EntityTarget,
|
||||
@@ -29,13 +29,13 @@ import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repo
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
export class WorkspaceInsertQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends InsertQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
@@ -47,7 +47,7 @@ export class WorkspaceInsertQueryBuilder<
|
||||
|
||||
constructor(
|
||||
queryBuilder: InsertQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type EntityTarget,
|
||||
type ObjectLiteral,
|
||||
@@ -30,14 +30,14 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g
|
||||
export class WorkspaceSelectQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends SelectQueryBuilder<T> {
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions: ObjectsPermissions;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
internalContext: WorkspaceInternalContext;
|
||||
authContext?: AuthContext;
|
||||
featureFlagMap?: FeatureFlagMap;
|
||||
constructor(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type EntityTarget,
|
||||
type InsertQueryBuilder,
|
||||
@@ -22,13 +22,13 @@ import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/reposito
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
export class WorkspaceSoftDeleteQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends SoftDeleteQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
@@ -36,7 +36,7 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
||||
|
||||
constructor(
|
||||
queryBuilder: SoftDeleteQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type EntityTarget,
|
||||
@@ -28,13 +28,13 @@ import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/wo
|
||||
import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
export class WorkspaceUpdateQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends UpdateQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
@@ -50,7 +50,7 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
|
||||
constructor(
|
||||
queryBuilder: UpdateQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type DeepPartial,
|
||||
type DeleteResult,
|
||||
@@ -40,7 +40,7 @@ export class WorkspaceRepository<
|
||||
private readonly internalContext: WorkspaceInternalContext;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
public readonly objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
public readonly objectRecordsPermissions?: ObjectsPermissions;
|
||||
private authContext?: AuthContext;
|
||||
declare manager: WorkspaceEntityManager;
|
||||
|
||||
@@ -50,7 +50,7 @@ export class WorkspaceRepository<
|
||||
manager: WorkspaceEntityManager,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
queryRunner?: QueryRunner,
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions?: ObjectsPermissions,
|
||||
shouldBypassPermissionChecks = false,
|
||||
authContext?: AuthContext,
|
||||
) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -15,30 +16,19 @@ export class TwentyORMGlobalManager {
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntity: Type<T>,
|
||||
options?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
},
|
||||
options?: RolePermissionConfig,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
objectMetadataName: string,
|
||||
options?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
},
|
||||
options?: RolePermissionConfig,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntityOrObjectMetadataName: Type<T> | string,
|
||||
options: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
} = {
|
||||
shouldBypassPermissionChecks: false,
|
||||
},
|
||||
options?: RolePermissionConfig,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
let objectMetadataName: string;
|
||||
|
||||
@@ -55,8 +45,7 @@ export class TwentyORMGlobalManager {
|
||||
|
||||
const repository = workspaceDataSource.getRepository<T>(
|
||||
objectMetadataName,
|
||||
options.shouldBypassPermissionChecks,
|
||||
options.roleId,
|
||||
options,
|
||||
);
|
||||
|
||||
return repository;
|
||||
|
||||
@@ -74,8 +74,7 @@ export class TwentyORMManager {
|
||||
|
||||
return workspaceDataSource.getRepository<T>(
|
||||
objectMetadataName,
|
||||
false,
|
||||
roleId,
|
||||
roleId ? { unionOf: [roleId] } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
type RoleId = string;
|
||||
|
||||
export type RolePermissionConfig =
|
||||
| { shouldBypassPermissionChecks: true }
|
||||
| { unionOf: RoleId[] }
|
||||
| { intersectionOf: RoleId[] };
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
|
||||
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
|
||||
|
||||
describe('computePermissionIntersection', () => {
|
||||
const objectMetadataId1 = 'object-1';
|
||||
const objectMetadataId2 = 'object-2';
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty object for empty array', () => {
|
||||
const result = computePermissionIntersection([]);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return same permissions for single role', () => {
|
||||
const permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([permissions]);
|
||||
|
||||
expect(result).toEqual(permissions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('intersection logic (AND)', () => {
|
||||
it('should require ALL roles to have permission (true AND true = true)', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should deny if ANY role lacks permission (true AND false = false)', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should deny all permissions if role lacks access to object entirely', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: false,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple objects', () => {
|
||||
it('should compute intersection independently for each object', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
[objectMetadataId2]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
[objectMetadataId2]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
});
|
||||
|
||||
expect(result[objectMetadataId2]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('restricted fields', () => {
|
||||
it('should compute intersection for restricted fields', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
email: {
|
||||
canRead: null,
|
||||
canUpdate: false,
|
||||
},
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
email: {
|
||||
canRead: null,
|
||||
canUpdate: null,
|
||||
},
|
||||
salary: {
|
||||
canRead: null,
|
||||
canUpdate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1].restrictedFields).toEqual({
|
||||
email: {
|
||||
canRead: null,
|
||||
canUpdate: false,
|
||||
},
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle fields that only exist in some roles', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
email: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1].restrictedFields).toEqual({
|
||||
email: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('three or more roles', () => {
|
||||
it('should compute intersection across all roles', () => {
|
||||
const role1: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role3: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([role1, role2, role3]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
type ObjectsPermissions,
|
||||
type RestrictedFieldPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const computePermissionIntersection = (
|
||||
permissionsArray: ObjectsPermissions[],
|
||||
): ObjectsPermissions => {
|
||||
if (permissionsArray.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (permissionsArray.length === 1) {
|
||||
return permissionsArray[0];
|
||||
}
|
||||
|
||||
const result: ObjectsPermissions = {};
|
||||
|
||||
const allObjectMetadataIds = new Set<string>();
|
||||
|
||||
for (const permissions of permissionsArray) {
|
||||
for (const id of Object.keys(permissions)) {
|
||||
allObjectMetadataIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const objectMetadataId of allObjectMetadataIds) {
|
||||
let canReadObjectRecords = true;
|
||||
let canUpdateObjectRecords = true;
|
||||
let canSoftDeleteObjectRecords = true;
|
||||
let canDestroyObjectRecords = true;
|
||||
const restrictedFields: Record<string, RestrictedFieldPermissions> = {};
|
||||
|
||||
for (const permissions of permissionsArray) {
|
||||
const objPerm = permissions[objectMetadataId];
|
||||
|
||||
if (!objPerm) {
|
||||
canReadObjectRecords = false;
|
||||
canUpdateObjectRecords = false;
|
||||
canSoftDeleteObjectRecords = false;
|
||||
canDestroyObjectRecords = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
canReadObjectRecords =
|
||||
canReadObjectRecords && objPerm.canReadObjectRecords === true;
|
||||
canUpdateObjectRecords =
|
||||
canUpdateObjectRecords && objPerm.canUpdateObjectRecords === true;
|
||||
canSoftDeleteObjectRecords =
|
||||
canSoftDeleteObjectRecords &&
|
||||
objPerm.canSoftDeleteObjectRecords === true;
|
||||
canDestroyObjectRecords =
|
||||
canDestroyObjectRecords && objPerm.canDestroyObjectRecords === true;
|
||||
|
||||
if (objPerm.restrictedFields) {
|
||||
for (const [fieldName, fieldPerm] of Object.entries(
|
||||
objPerm.restrictedFields,
|
||||
)) {
|
||||
if (!restrictedFields[fieldName]) {
|
||||
restrictedFields[fieldName] = {
|
||||
canRead: null,
|
||||
canUpdate: null,
|
||||
};
|
||||
}
|
||||
|
||||
const current = restrictedFields[fieldName];
|
||||
|
||||
restrictedFields[fieldName] = {
|
||||
canRead:
|
||||
current.canRead === false || fieldPerm.canRead === false
|
||||
? false
|
||||
: null,
|
||||
canUpdate:
|
||||
current.canUpdate === false || fieldPerm.canUpdate === false
|
||||
? false
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result[objectMetadataId] = {
|
||||
canReadObjectRecords,
|
||||
canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords,
|
||||
restrictedFields,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user