implement "acting on behalf of user" for workflows and agents (#15103)

## Summary
**Step 1 of 2:** Implements the "acting on behalf of user" concept for
workflows and agents to prevent permission escalation and maintain
proper audit trails.

## Problem
Previously, workflows and agents would bypass permissions regardless of
who initiated them, allowing users to escalate their privileges by
triggering workflows that performed actions they couldn't do directly.

## Solution

### For Workflows
Introduced `WorkflowExecutionContext` service that determines execution
mode:
- **Manual triggers/test button**: Uses user's roleId for permissions,
user's identity for `createdBy`
- **Automated triggers** (cron, database events, webhooks): Bypasses
permissions, uses workflow identity

### For Agents
**In Chat:**
- Always act on behalf of the user
- Use user's roleId for permission checks
- Use user's identity for `createdBy`


# Step 1 vs Step 2

###  Step 1 (This PR): Acting on Behalf Concept
- Introduced `isActingOnBehalfOfUser` boolean concept
- Single roleId used for permission checks (user's OR system bypass)
- `createdBy` field properly attributes actions to initiator
- Prevents permission escalation in user-initiated flows

### 🔜 Step 2 (Future): Multi-Role Permission Support
- Support role intersection: `{ intersection: ['roleA', 'roleB'] }`
- Support role union: `{ union: ['roleA', 'roleB', 'roleC'] }`
- Enable user+agent collaboration scenarios
- Update `WorkspaceEntityManager` and `WorkspaceDatasource` to handle
multiple roleIds

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Abdul Rahman
2025-10-18 02:26:28 +05:30
committed by GitHub
parent 434df8a94c
commit a3f9657b73
17 changed files with 276 additions and 26 deletions
@@ -14,6 +14,7 @@ import {
getRecordInputSchema,
} from 'src/engine/metadata-modules/agent/utils/agent-tool-schema.utils';
import { isWorkflowRunObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-run-object.util';
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -30,7 +31,11 @@ export class ToolService {
private readonly findRecordsService: FindRecordsService,
) {}
async listTools(roleId: string, workspaceId: string): Promise<ToolSet> {
async listTools(
roleId: string,
workspaceId: string,
actorContext?: ActorMetadata,
): Promise<ToolSet> {
const tools: ToolSet = {};
const { data: rolesPermissions } =
@@ -70,6 +75,7 @@ export class ToolService {
objectRecord: parameters.input,
workspaceId,
roleId,
createdBy: actorContext,
});
},
};
@@ -85,9 +85,9 @@ export class CreateRecordService {
const insertResult = await repository.insert({
...transformedObjectRecord,
position,
createdBy: {
source: roleId ? FieldActorSource.AGENT : FieldActorSource.WORKFLOW,
name: roleId ? 'Agent' : 'Workflow',
createdBy: params.createdBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
},
});
@@ -1,8 +1,10 @@
import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type';
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
export type CreateRecordParams = {
objectName: string;
objectRecord: ObjectRecordProperties;
workspaceId: string;
roleId?: string;
createdBy?: ActorMetadata;
};