Files
twenty/packages/twenty-server/src/engine/metadata-modules/agent/services/agent-actor-context.service.ts
T
Abdul Rahman a3f9657b73 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>
2025-10-17 22:56:28 +02:00

72 lines
2.2 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
import { UserWorkspaceService as UserService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/agent/agent.exception';
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export type AgentActorContext = {
actorContext: ActorMetadata;
roleId: string | undefined;
};
@Injectable()
export class AgentActorContextService {
constructor(
private readonly userService: UserService,
private readonly userRoleService: UserRoleService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async buildUserActorContext(
userWorkspaceId: string,
workspaceId: string,
): Promise<AgentActorContext> {
const userWorkspace = await this.userService.findById(userWorkspaceId);
if (!userWorkspace) {
throw new AgentException(
'User workspace not found',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: userWorkspace.userId,
},
});
if (!workspaceMember) {
throw new AgentException(
'Workspace member not found for user',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
userWorkspaceId,
workspaceId,
});
const actorContext = buildCreatedByFromFullNameMetadata({
fullNameMetadata: workspaceMember.name,
workspaceMemberId: workspaceMember.id,
});
return { actorContext, roleId };
}
}