74b6466a57
This PR introduces a significant enhancement to the role-based permission system by extending it to support AI agents, enabling them to perform database operations based on assigned permissions. ## Key Changes ### 1. Database Schema Migration - **Table Rename**: `userWorkspaceRole` → `roleTargets` to better reflect its expanded purpose - **New Column**: Added `agentId` (UUID, nullable) to support AI agent role assignments - **Constraint Updates**: - Made `userWorkspaceId` nullable to accommodate agent-only role assignments - Added check constraint `CHK_role_targets_either_agent_or_user` ensuring either `agentId` OR `userWorkspaceId` is set (not both) ### 2. Entity & Service Layer Updates - **RoleTargetsEntity**: Updated with new `agentId` field and constraint validation - **AgentRoleService**: New service for managing agent role assignments with validation - **AgentService**: Enhanced to include role information when retrieving agents - **RoleResolver**: Added GraphQL mutations for `assignRoleToAgent` and `removeRoleFromAgent` ### 3. AI Agent CRUD Operations - **Permission-Based Tool Generation**: AI agents now receive database tools based on their assigned role permissions - **Dynamic Tool Creation**: The `AgentToolService` generates CRUD tools (`create_*`, `find_*`, `update_*`, `soft_delete_*`, `destroy_*`) for each object based on role permissions - **Granular Permissions**: Supports both global role permissions (`canReadAllObjectRecords`) and object-specific permissions (`canReadObjectRecords`) ### 4. Frontend Integration - **Role Assignment UI**: Added hooks and components for assigning/removing roles from agents ## Demo https://github.com/user-attachments/assets/41732267-742e-416c-b423-b687c2614c82 --------- Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Guillim <guillim@users.noreply.github.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: Baptiste Devessier <baptiste@devessier.fr> Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: prastoin <paul@twenty.com> Co-authored-by: Vicky Wang <157669812+vickywxng@users.noreply.github.com> Co-authored-by: Vicky Wang <vw92@cornell.edu> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
198 lines
7.0 KiB
TypeScript
198 lines
7.0 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { Repository } from 'typeorm';
|
|
|
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
|
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
|
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
|
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
|
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
import { standardObjectsPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data';
|
|
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
|
|
|
|
@Injectable()
|
|
export class WorkspaceManagerService {
|
|
private readonly logger = new Logger(WorkspaceManagerService.name);
|
|
|
|
constructor(
|
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
|
private readonly objectMetadataService: ObjectMetadataService,
|
|
private readonly dataSourceService: DataSourceService,
|
|
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
|
@InjectRepository(FieldMetadataEntity, 'core')
|
|
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
@InjectRepository(UserWorkspace, 'core')
|
|
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
|
private readonly roleService: RoleService,
|
|
private readonly userRoleService: UserRoleService,
|
|
private readonly featureFlagService: FeatureFlagService,
|
|
@InjectRepository(Workspace, 'core')
|
|
private readonly workspaceRepository: Repository<Workspace>,
|
|
@InjectRepository(RoleEntity, 'core')
|
|
private readonly roleRepository: Repository<RoleEntity>,
|
|
@InjectRepository(RoleTargetsEntity, 'core')
|
|
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
|
) {}
|
|
|
|
public async init({
|
|
workspaceId,
|
|
userId,
|
|
}: {
|
|
workspaceId: string;
|
|
userId: string;
|
|
}): Promise<void> {
|
|
const schemaCreationStart = performance.now();
|
|
const schemaName =
|
|
await this.workspaceDataSourceService.createWorkspaceDBSchema(
|
|
workspaceId,
|
|
);
|
|
|
|
const schemaCreationEnd = performance.now();
|
|
|
|
this.logger.log(
|
|
`Schema creation took ${schemaCreationEnd - schemaCreationStart}ms`,
|
|
);
|
|
|
|
const dataSourceMetadataCreationStart = performance.now();
|
|
const dataSourceMetadata =
|
|
await this.dataSourceService.createDataSourceMetadata(
|
|
workspaceId,
|
|
schemaName,
|
|
);
|
|
|
|
const featureFlags =
|
|
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
|
|
|
|
await this.workspaceSyncMetadataService.synchronize({
|
|
workspaceId,
|
|
dataSourceId: dataSourceMetadata.id,
|
|
featureFlags,
|
|
});
|
|
|
|
const dataSourceMetadataCreationEnd = performance.now();
|
|
|
|
this.logger.log(
|
|
`Metadata creation took ${dataSourceMetadataCreationEnd - dataSourceMetadataCreationStart}ms`,
|
|
);
|
|
|
|
const permissionsEnabledStart = performance.now();
|
|
|
|
await this.initPermissions({ workspaceId, userId });
|
|
|
|
const permissionsEnabledEnd = performance.now();
|
|
|
|
this.logger.log(
|
|
`Permissions enabled took ${permissionsEnabledEnd - permissionsEnabledStart}ms`,
|
|
);
|
|
|
|
const prefillStandardObjectsStart = performance.now();
|
|
|
|
await this.prefillWorkspaceWithStandardObjectsRecords(
|
|
dataSourceMetadata,
|
|
workspaceId,
|
|
);
|
|
|
|
const prefillStandardObjectsEnd = performance.now();
|
|
|
|
this.logger.log(
|
|
`Prefill standard objects took ${prefillStandardObjectsEnd - prefillStandardObjectsStart}ms`,
|
|
);
|
|
}
|
|
|
|
private async prefillWorkspaceWithStandardObjectsRecords(
|
|
dataSourceMetadata: DataSourceEntity,
|
|
workspaceId: string,
|
|
) {
|
|
const mainDataSource =
|
|
await this.workspaceDataSourceService.connectToMainDataSource();
|
|
|
|
if (!mainDataSource) {
|
|
throw new Error('Could not connect to main data source');
|
|
}
|
|
|
|
const createdObjectMetadata =
|
|
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
|
|
|
await standardObjectsPrefillData(
|
|
mainDataSource,
|
|
dataSourceMetadata.schema,
|
|
createdObjectMetadata,
|
|
);
|
|
}
|
|
|
|
public async delete(workspaceId: string): Promise<void> {
|
|
//TODO: delete all logs when #611 closed
|
|
this.logger.log(`Deleting workspace ${workspaceId} ...`);
|
|
|
|
await this.fieldMetadataRepository.delete({
|
|
workspaceId,
|
|
});
|
|
this.logger.log(`workspace ${workspaceId} field metadata deleted`);
|
|
|
|
await this.roleTargetsRepository.delete({
|
|
workspaceId,
|
|
});
|
|
this.logger.log(`workspace ${workspaceId} role targets deleted`);
|
|
|
|
await this.roleRepository.delete({
|
|
workspaceId,
|
|
});
|
|
this.logger.log(`workspace ${workspaceId} role deleted`);
|
|
|
|
await this.objectMetadataService.deleteObjectsMetadata(workspaceId);
|
|
this.logger.log(`workspace ${workspaceId} object metadata deleted`);
|
|
|
|
await this.workspaceMigrationService.deleteAllWithinWorkspace(workspaceId);
|
|
this.logger.log(`workspace ${workspaceId} migration deleted`);
|
|
|
|
await this.dataSourceService.delete(workspaceId);
|
|
this.logger.log(`workspace ${workspaceId} data source deleted`);
|
|
// Delete schema
|
|
await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId);
|
|
this.logger.log(`workspace ${workspaceId} schema deleted`);
|
|
}
|
|
|
|
private async initPermissions({
|
|
workspaceId,
|
|
userId,
|
|
}: {
|
|
workspaceId: string;
|
|
userId: string;
|
|
}) {
|
|
const adminRole = await this.roleService.createAdminRole({
|
|
workspaceId,
|
|
});
|
|
|
|
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
|
|
where: {
|
|
workspaceId,
|
|
userId,
|
|
},
|
|
});
|
|
|
|
await this.userRoleService.assignRoleToUserWorkspace({
|
|
workspaceId,
|
|
userWorkspaceId: userWorkspace.id,
|
|
roleId: adminRole.id,
|
|
});
|
|
|
|
const memberRole = await this.roleService.createMemberRole({
|
|
workspaceId,
|
|
});
|
|
|
|
await this.workspaceRepository.update(workspaceId, {
|
|
defaultRoleId: memberRole.id,
|
|
});
|
|
}
|
|
}
|