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:
+10
-16
@@ -1,5 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
convertToModelMessages,
|
||||
@@ -13,15 +12,15 @@ import {
|
||||
} from 'ai';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
|
||||
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
|
||||
import { AgentActorContextService } from 'src/engine/metadata-modules/agent/services/agent-actor-context.service';
|
||||
@@ -48,7 +47,6 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
|
||||
constructor(
|
||||
private readonly agentHandoffToolService: AgentHandoffToolService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
@@ -57,8 +55,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
private readonly agentModelConfigService: AgentModelConfigService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
private readonly agentActorContextService: AgentActorContextService,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
private readonly agentService: AgentService,
|
||||
) {}
|
||||
|
||||
async prepareAIRequestConfig({
|
||||
@@ -66,14 +63,14 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
system,
|
||||
agent,
|
||||
actorContext,
|
||||
roleIdOverride,
|
||||
roleIds,
|
||||
excludeHandoffTools = false,
|
||||
}: {
|
||||
system: string;
|
||||
agent: AgentEntity | null;
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
actorContext?: ActorMetadata;
|
||||
roleIdOverride?: string;
|
||||
roleIds?: string[];
|
||||
excludeHandoffTools?: boolean;
|
||||
}) {
|
||||
try {
|
||||
@@ -95,7 +92,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
roleIdOverride,
|
||||
roleIds,
|
||||
);
|
||||
|
||||
let handoffTools = {};
|
||||
@@ -193,8 +190,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
|
||||
false,
|
||||
roleId,
|
||||
{ unionOf: [roleId] },
|
||||
);
|
||||
|
||||
const restrictedFields =
|
||||
@@ -254,9 +250,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType;
|
||||
}) {
|
||||
try {
|
||||
const agent = await this.agentRepository.findOneOrFail({
|
||||
where: { id: agentId },
|
||||
});
|
||||
const agent = await this.agentService.findOneAgent(agentId, workspace.id);
|
||||
|
||||
let contextString = '';
|
||||
|
||||
@@ -271,7 +265,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
}
|
||||
|
||||
const { actorContext, roleId } =
|
||||
await this.agentActorContextService.buildUserActorContext(
|
||||
await this.agentActorContextService.buildUserAndAgentActorContext(
|
||||
userWorkspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
@@ -281,7 +275,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
agent,
|
||||
messages,
|
||||
actorContext,
|
||||
roleIdOverride: roleId,
|
||||
roleIds: [roleId, ...(agent?.roleId ? [agent?.roleId] : [])],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
|
||||
Reference in New Issue
Block a user