a3f9657b73
## 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>
72 lines
2.2 KiB
TypeScript
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 };
|
|
}
|
|
}
|