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:
+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,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user