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
@@ -79,19 +79,18 @@ export abstract class CommonBaseQueryRunnerService {
workspace.id,
);
const rolePermissionConfig = { unionOf: [roleId] };
const repository = workspaceDataSource.getRepository(
objectMetadataItemWithFieldMaps.nameSingular,
false,
roleId,
rolePermissionConfig,
authContext,
);
return {
workspaceDataSource,
repository,
isExecutedByApiKey: isDefined(authContext.apiKey),
roleId,
shouldBypassPermissionChecks: false,
rolePermissionConfig,
};
}
@@ -31,6 +31,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
@Injectable()
export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService {
@@ -62,15 +63,11 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
}
});
const {
workspaceDataSource,
repository,
roleId,
shouldBypassPermissionChecks,
} = await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const { workspaceDataSource, repository, rolePermissionConfig } =
await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const processedArgs = await this.processQueryArgs({
authContext,
@@ -98,10 +95,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
records: upsertedRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
roleId,
rolePermissionConfig,
authContext,
workspaceDataSource,
shouldBypassPermissionChecks,
});
return upsertedRecords;
@@ -371,19 +367,17 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
records,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
roleId,
rolePermissionConfig,
authContext,
workspaceDataSource,
shouldBypassPermissionChecks,
}: {
args: CreateManyQueryArgs;
records: ObjectRecord[];
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
roleId?: string;
rolePermissionConfig?: RolePermissionConfig;
authContext: AuthContext;
workspaceDataSource: WorkspaceDataSource;
shouldBypassPermissionChecks: boolean;
}): Promise<void> {
if (!args.selectedFieldsResult.relations) {
return;
@@ -401,8 +395,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
roleId,
shouldBypassPermissionChecks,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
});
}
@@ -60,15 +60,11 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
);
}
const {
workspaceDataSource,
repository,
roleId,
shouldBypassPermissionChecks,
} = await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const { workspaceDataSource, repository, rolePermissionConfig } =
await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const processedArgs = await this.processQueryArgs({
authContext,
@@ -195,8 +191,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
roleId,
shouldBypassPermissionChecks,
rolePermissionConfig,
selectedFields: processedArgs.selectedFieldsResult.select,
});
}
@@ -46,15 +46,11 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
);
}
const {
workspaceDataSource,
repository,
roleId,
shouldBypassPermissionChecks,
} = await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const { workspaceDataSource, repository, rolePermissionConfig } =
await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const processedArgs = await this.processQueryArgs({
authContext,
@@ -128,8 +124,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
roleId,
shouldBypassPermissionChecks,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
});
}