Files
twenty/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts
T
Abdul Rahman 3bec43696f 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>
2025-10-19 09:30:05 +02:00

91 lines
2.9 KiB
TypeScript

import { Injectable, type Type } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectLiteral, Repository } from 'typeorm';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
@Injectable()
export class TwentyORMManager {
constructor(
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
) {}
async getRepository<T extends ObjectLiteral>(
workspaceEntity: Type<T>,
): Promise<WorkspaceRepository<T>>;
async getRepository<T extends ObjectLiteral>(
objectMetadataName: string,
): Promise<WorkspaceRepository<T>>;
async getRepository<T extends ObjectLiteral>(
workspaceEntityOrObjectMetadataName: Type<T> | string,
): Promise<WorkspaceRepository<T>> {
const { workspaceId, userWorkspaceId, apiKeyId } =
this.scopedWorkspaceContextFactory.create();
let objectMetadataName: string;
if (typeof workspaceEntityOrObjectMetadataName === 'string') {
objectMetadataName = workspaceEntityOrObjectMetadataName;
} else {
objectMetadataName = convertClassNameToObjectMetadataName(
workspaceEntityOrObjectMetadataName.name,
);
}
if (!workspaceId) {
throw new Error('Workspace not found');
}
const workspaceDataSource =
await this.workspaceDataSourceFactory.create(workspaceId);
let roleId: string | undefined;
if (isDefined(userWorkspaceId)) {
const roleTarget = await this.roleTargetsRepository.findOne({
where: {
userWorkspaceId,
workspaceId,
},
});
roleId = roleTarget?.roleId;
} else if (isDefined(apiKeyId)) {
const roleTarget = await this.roleTargetsRepository.findOne({
where: {
apiKeyId,
workspaceId,
},
});
roleId = roleTarget?.roleId;
}
return workspaceDataSource.getRepository<T>(
objectMetadataName,
roleId ? { unionOf: [roleId] } : undefined,
);
}
async getDatasource() {
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
if (!workspaceId) {
throw new Error('Workspace not found');
}
return this.workspaceDataSourceFactory.create(workspaceId);
}
}