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:
Abdul Rahman
2025-10-19 13:00:05 +05:30
committed by GitHub
parent f6d133f285
commit 3bec43696f
75 changed files with 909 additions and 489 deletions
@@ -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;
};