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:
+5
-1
@@ -49,10 +49,14 @@ export class WorkflowExecutionContextService {
|
||||
});
|
||||
}
|
||||
|
||||
const rolePermissionConfig = roleId
|
||||
? { unionOf: [roleId] }
|
||||
: { shouldBypassPermissionChecks: true as const };
|
||||
|
||||
return {
|
||||
isActingOnBehalfOfUser,
|
||||
initiator: workflowRun.createdBy,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type WorkflowExecutionContext = {
|
||||
isActingOnBehalfOfUser: boolean;
|
||||
initiator: ActorMetadata;
|
||||
roleId?: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
};
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
actorContext: executionContext.isActingOnBehalfOfUser
|
||||
? executionContext.initiator
|
||||
: undefined,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+28
-10
@@ -18,6 +18,7 @@ import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constant
|
||||
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
|
||||
@Injectable()
|
||||
@@ -35,7 +36,7 @@ export class AiAgentExecutorService {
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
roleIdOverride?: string,
|
||||
rolePermissionConfig?: RolePermissionConfig,
|
||||
): Promise<ToolSet> {
|
||||
const roleTarget = await this.roleTargetsRepository.findOne({
|
||||
where: {
|
||||
@@ -45,21 +46,33 @@ export class AiAgentExecutorService {
|
||||
select: ['roleId'],
|
||||
});
|
||||
|
||||
const effectiveRoleId = roleIdOverride || roleTarget?.roleId;
|
||||
const agentRoleId = roleTarget?.roleId;
|
||||
|
||||
if (!effectiveRoleId) {
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
if (!rolePermissionConfig && !agentRoleId) {
|
||||
return await this.toolAdapterService.getTools();
|
||||
}
|
||||
|
||||
return { ...actionTools };
|
||||
let effectiveRoleContext: RolePermissionConfig;
|
||||
|
||||
if (
|
||||
rolePermissionConfig &&
|
||||
('intersectionOf' in rolePermissionConfig ||
|
||||
'unionOf' in rolePermissionConfig)
|
||||
) {
|
||||
effectiveRoleContext = rolePermissionConfig;
|
||||
} else if (agentRoleId) {
|
||||
effectiveRoleContext = { unionOf: [agentRoleId] };
|
||||
} else {
|
||||
return await this.toolAdapterService.getTools();
|
||||
}
|
||||
|
||||
const actionTools = await this.toolAdapterService.getTools(
|
||||
effectiveRoleId,
|
||||
effectiveRoleContext,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
effectiveRoleId,
|
||||
effectiveRoleContext,
|
||||
workspaceId,
|
||||
actorContext,
|
||||
);
|
||||
@@ -75,20 +88,25 @@ export class AiAgentExecutorService {
|
||||
schema,
|
||||
userPrompt,
|
||||
actorContext,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
agent: AgentEntity | null;
|
||||
schema: OutputSchema;
|
||||
userPrompt: string;
|
||||
actorContext?: ActorMetadata;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
}): Promise<AgentExecutionResult> {
|
||||
try {
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
const tools = agent
|
||||
? await this.getTools(agent.id, agent.workspaceId, actorContext, roleId)
|
||||
? await this.getTools(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
rolePermissionConfig,
|
||||
)
|
||||
: {};
|
||||
|
||||
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
workspaceId,
|
||||
createdBy,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ export class DeleteRecordWorkflowAction implements WorkflowAction {
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecordId: workflowActionInput.objectRecordId,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
soft: true,
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
orderBy: workflowActionInput.orderBy,
|
||||
limit: workflowActionInput.limit,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
+17
-13
@@ -8,6 +8,7 @@ import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/wor
|
||||
import type { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto';
|
||||
import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
|
||||
@@ -43,7 +44,10 @@ export class WorkflowToolWorkspaceService {
|
||||
private readonly recordPositionService: RecordPositionService,
|
||||
) {}
|
||||
|
||||
generateWorkflowTools(workspaceId: string, roleId: string): ToolSet {
|
||||
generateWorkflowTools(
|
||||
workspaceId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
): ToolSet {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
tools.create_complete_workflow = {
|
||||
@@ -88,7 +92,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
const workflowId = await this.createWorkflow({
|
||||
workspaceId,
|
||||
name: parameters.name,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
|
||||
const workflowVersionId = await this.createWorkflowVersion({
|
||||
@@ -96,7 +100,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workflowId,
|
||||
trigger: parameters.trigger,
|
||||
steps: parameters.steps,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (parameters.stepPositions && parameters.stepPositions.length > 0) {
|
||||
@@ -132,7 +136,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workspaceId,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -400,17 +404,17 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
private async createWorkflow({
|
||||
workspaceId,
|
||||
name,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
}): Promise<string> {
|
||||
const workflowRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ roleId },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const workflowPosition =
|
||||
@@ -440,19 +444,19 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workflowId,
|
||||
trigger,
|
||||
steps,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowId: string;
|
||||
trigger: WorkflowTrigger;
|
||||
steps: WorkflowAction[];
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
}): Promise<string> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ roleId },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const versionPosition =
|
||||
@@ -484,18 +488,18 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workspaceId,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowId: string;
|
||||
workflowVersionId: string;
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
}) {
|
||||
const workflowRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ roleId },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
await workflowRepository.update(workflowId, {
|
||||
|
||||
Reference in New Issue
Block a user