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,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;
}