feat: Add agent role assignment and database CRUD tools for AI agent nodes (#12888)
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>
This commit is contained in:
@ -4,7 +4,7 @@ import { Relation } from 'typeorm';
|
||||
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto';
|
||||
|
||||
@ObjectType('Role')
|
||||
@ -25,7 +25,7 @@ export class RoleDTO {
|
||||
isEditable: boolean;
|
||||
|
||||
@HideField()
|
||||
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
|
||||
roleTargets: Relation<RoleTargetsEntity[]>;
|
||||
|
||||
@Field(() => [WorkspaceMember], { nullable: true })
|
||||
workspaceMembers?: WorkspaceMember[];
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
Check,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -13,16 +14,15 @@ import {
|
||||
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
|
||||
@Entity('userWorkspaceRole')
|
||||
@Unique('IDX_USER_WORKSPACE_ROLE_USER_WORKSPACE_ID_ROLE_ID_UNIQUE', [
|
||||
'userWorkspaceId',
|
||||
'roleId',
|
||||
])
|
||||
@Index('IDX_USER_WORKSPACE_ROLE_USER_WORKSPACE_ID_WORKSPACE_ID', [
|
||||
'userWorkspaceId',
|
||||
'workspaceId',
|
||||
])
|
||||
export class UserWorkspaceRoleEntity {
|
||||
@Entity('roleTargets')
|
||||
@Unique('IDX_ROLE_TARGETS_UNIQUE', ['userWorkspaceId', 'roleId', 'agentId'])
|
||||
@Index('IDX_ROLE_TARGETS_WORKSPACE_ID', ['userWorkspaceId', 'workspaceId'])
|
||||
@Index('IDX_ROLE_TARGETS_AGENT_ID', ['agentId'])
|
||||
@Check(
|
||||
'CHK_role_targets_either_agent_or_user',
|
||||
'("agentId" IS NOT NULL AND "userWorkspaceId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NOT NULL)',
|
||||
)
|
||||
export class RoleTargetsEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ -32,15 +32,18 @@ export class UserWorkspaceRoleEntity {
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
roleId: string;
|
||||
|
||||
@ManyToOne(() => RoleEntity, (role) => role.userWorkspaceRoles, {
|
||||
@ManyToOne(() => RoleEntity, (role) => role.roleTargets, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'roleId' })
|
||||
role: Relation<RoleEntity>;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
userWorkspaceId: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
agentId: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
|
||||
|
||||
@Entity('role')
|
||||
@ -56,10 +56,10 @@ export class RoleEntity {
|
||||
isEditable: boolean;
|
||||
|
||||
@OneToMany(
|
||||
() => UserWorkspaceRoleEntity,
|
||||
(userWorkspaceRole: UserWorkspaceRoleEntity) => userWorkspaceRole.role,
|
||||
() => RoleTargetsEntity,
|
||||
(roleTargets: RoleTargetsEntity) => roleTargets.role,
|
||||
)
|
||||
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
|
||||
roleTargets: Relation<RoleTargetsEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => ObjectPermissionEntity,
|
||||
|
||||
@ -5,8 +5,10 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
|
||||
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
@ -16,9 +18,10 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RoleEntity], 'core'),
|
||||
TypeOrmModule.forFeature([RoleEntity, RoleTargetsEntity], 'core'),
|
||||
TypeOrmModule.forFeature([UserWorkspace, Workspace], 'core'),
|
||||
UserRoleModule,
|
||||
AgentRoleModule,
|
||||
PermissionsModule,
|
||||
UserWorkspaceModule,
|
||||
ObjectPermissionModule,
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@ -15,9 +17,11 @@ import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-mem
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
|
||||
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
@ -55,6 +59,7 @@ export class RoleResolver {
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly objectPermissionService: ObjectPermissionService,
|
||||
private readonly settingPermissionService: SettingPermissionService,
|
||||
private readonly agentRoleService: AgentRoleService,
|
||||
) {}
|
||||
|
||||
@Query(() => [RoleDTO])
|
||||
@ -174,6 +179,36 @@ export class RoleResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async assignRoleToAgent(
|
||||
@Args('agentId', { type: () => UUIDScalarType }) agentId: string,
|
||||
@Args('roleId', { type: () => UUIDScalarType }) roleId: string,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
await this.agentRoleService.assignRoleToAgent({
|
||||
agentId,
|
||||
roleId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async removeRoleFromAgent(
|
||||
@Args('agentId', { type: () => UUIDScalarType }) agentId: string,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
await this.agentRoleService.removeRoleFromAgent({
|
||||
agentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ResolveField('workspaceMembers', () => [WorkspaceMember])
|
||||
async getWorkspaceMembersAssignedToRole(
|
||||
@Parent() role: RoleDTO,
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
UpdateRoleInput,
|
||||
UpdateRolePayload,
|
||||
} from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util';
|
||||
@ -27,6 +28,8 @@ export class RoleService {
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
@ -36,11 +39,7 @@ export class RoleService {
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
relations: [
|
||||
'userWorkspaceRoles',
|
||||
'settingPermissions',
|
||||
'objectPermissions',
|
||||
],
|
||||
relations: ['roleTargets', 'settingPermissions', 'objectPermissions'],
|
||||
});
|
||||
}
|
||||
|
||||
@ -53,7 +52,7 @@ export class RoleService {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['userWorkspaceRoles', 'settingPermissions'],
|
||||
relations: ['roleTargets', 'settingPermissions'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,31 +1,19 @@
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
|
||||
export const fromRoleEntityToRoleDto = ({
|
||||
id,
|
||||
label,
|
||||
canUpdateAllSettings,
|
||||
description,
|
||||
icon,
|
||||
isEditable,
|
||||
userWorkspaceRoles,
|
||||
canReadAllObjectRecords,
|
||||
canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords,
|
||||
}: RoleEntity): RoleDTO => {
|
||||
export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
canUpdateAllSettings,
|
||||
description,
|
||||
icon,
|
||||
isEditable,
|
||||
userWorkspaceRoles,
|
||||
canReadAllObjectRecords,
|
||||
canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords,
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
canUpdateAllSettings: role.canUpdateAllSettings,
|
||||
description: role.description,
|
||||
icon: role.icon,
|
||||
isEditable: role.isEditable,
|
||||
canReadAllObjectRecords: role.canReadAllObjectRecords,
|
||||
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
|
||||
roleTargets: role.roleTargets,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user