Feat: role applicability controls (#14239)

Closes [#1404](https://github.com/twentyhq/core-team-issues/issues/1404)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-09-04 08:58:50 +05:30
committed by GitHub
parent 052f91cff1
commit 79bcd90d8d
72 changed files with 1889 additions and 344 deletions
@@ -49,11 +49,27 @@ export class WorkspaceAgentComparator {
switch (difference.type) {
case 'CREATE': {
const fromAgent = fromFlatAgents.find(
(agent) => keyFactory(agent) === universalIdentifier,
);
const toAgent = toFlatAgents.find(
(agent) => keyFactory(agent) === universalIdentifier,
);
if (toAgent) {
if (!toAgent) {
break;
}
if (fromAgent) {
fromAgent &&
results.push({
action: ComparatorAction.UPDATE,
object: {
...toAgent,
id: fromAgent.id,
},
});
} else {
results.push({
action: ComparatorAction.CREATE,
object: toAgent,
@@ -1,11 +1,12 @@
import { Injectable, Logger } from '@nestjs/common';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { IsNull, Not, type EntityManager, type Repository } from 'typeorm';
import { IsNull, Not, type EntityManager } from 'typeorm';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
@@ -19,7 +20,6 @@ import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace
import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory';
import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents';
import { WORKFLOW_CREATION_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent';
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
@Injectable()
export class WorkspaceSyncAgentService {
@@ -28,6 +28,7 @@ export class WorkspaceSyncAgentService {
constructor(
private readonly standardAgentFactory: StandardAgentFactory,
private readonly workspaceAgentComparator: WorkspaceAgentComparator,
private readonly agentRoleService: AgentRoleService,
) {}
async synchronize(
@@ -37,8 +38,6 @@ export class WorkspaceSyncAgentService {
this.logger.log('Syncing standard agent.');
const agentRepository = manager.getRepository(AgentEntity);
const roleRepository = manager.getRepository(RoleEntity);
const roleTargetsRepository = manager.getRepository(RoleTargetsEntity);
const existingStandardAgentEntities = await agentRepository.find({
where: {
@@ -75,13 +74,43 @@ export class WorkspaceSyncAgentService {
workspaceId: context.workspaceId,
});
await this.assignAdminRoleToAgent(
createdAgent.id,
context.workspaceId,
roleRepository,
roleTargetsRepository,
const agentDefinition = standardAgentDefinitions.find(
(def) => def.standardId === createdAgent.standardId,
);
if (agentDefinition?.standardRoleId) {
try {
const roleRepository = manager.getRepository(RoleEntity);
const role = await roleRepository.findOne({
where: {
standardId: agentDefinition.standardRoleId,
workspaceId: context.workspaceId,
},
});
if (!role) {
throw new Error(
`Standard role with standard ID ${agentDefinition.standardRoleId} not found in workspace`,
);
}
const roleTargetsRepository =
manager.getRepository(RoleTargetsEntity);
await roleTargetsRepository.save({
roleId: role.id,
agentId: createdAgent.id,
workspaceId: context.workspaceId,
});
} catch (error) {
this.logger.error(
`Failed to assign standard role ${agentDefinition.standardRoleId} to agent ${createdAgent.id}: ${error.message}`,
error.stack,
);
throw error;
}
}
if (createdAgent.standardId === WORKFLOW_CREATION_AGENT.standardId) {
await this.createAgentHandoffToWorkflowCreationAgent(
createdAgent.id,
@@ -99,9 +128,47 @@ export class WorkspaceSyncAgentService {
'id',
'universalIdentifier',
'workspaceId',
'standardRoleId' as keyof typeof agentToUpdate,
]);
await agentRepository.update({ id: agentToUpdate.id }, flatAgentData);
const agentDefinition = standardAgentDefinitions.find(
(def) => def.standardId === agentToUpdate.standardId,
);
if (agentDefinition?.standardRoleId) {
try {
const roleRepository = manager.getRepository(RoleEntity);
const role = await roleRepository.findOne({
where: {
standardId: agentDefinition.standardRoleId,
workspaceId: context.workspaceId,
},
});
if (!role) {
throw new Error(
`Standard role with standard ID ${agentDefinition.standardRoleId} not found in workspace`,
);
}
const roleTargetsRepository =
manager.getRepository(RoleTargetsEntity);
await roleTargetsRepository.save({
roleId: role.id,
agentId: agentToUpdate.id,
workspaceId: context.workspaceId,
});
} catch (error) {
this.logger.error(
`Failed to assign standard role ${agentDefinition.standardRoleId} to agent ${agentToUpdate.id}: ${error.message}`,
error.stack,
);
throw error;
}
}
break;
}
@@ -115,60 +182,6 @@ export class WorkspaceSyncAgentService {
}
}
private async assignAdminRoleToAgent(
agentId: string,
workspaceId: string,
roleRepository: Repository<RoleEntity>,
roleTargetsRepository: Repository<RoleTargetsEntity>,
): Promise<void> {
try {
const adminRole = await roleRepository.findOne({
where: {
workspaceId,
standardId: ADMIN_ROLE.standardId,
},
});
if (!adminRole) {
this.logger.warn(
`Admin role not found for workspace ${workspaceId}, cannot assign to agent ${agentId}.`,
);
return;
}
const existingRoleTarget = await roleTargetsRepository.findOne({
where: {
agentId,
roleId: adminRole.id,
workspaceId,
},
});
if (existingRoleTarget) {
this.logger.log(
`Workflow creation agent already has admin role assigned`,
);
return;
}
await roleTargetsRepository.save({
roleId: adminRole.id,
agentId,
workspaceId,
});
this.logger.log(
`Successfully assigned admin role to workflow creation agent`,
);
} catch (error) {
this.logger.error(
`Failed to assign admin role to workflow creation agent: ${error.message}`,
);
}
}
private async createAgentHandoffToWorkflowCreationAgent(
workflowCreationAgentId: string,
workspaceId: string,
@@ -1,12 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { IsNull, Not, type EntityManager } from 'typeorm';
import { IsNull, Not, type EntityManager, type Repository } from 'typeorm';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator';
import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory';
@@ -28,12 +30,15 @@ export class WorkspaceSyncRoleService {
this.logger.log('Syncing standard role metadata');
const roleRepository = manager.getRepository(RoleEntity);
const permissionFlagRepository =
manager.getRepository(PermissionFlagEntity);
const existingStandardRoleEntities = await roleRepository.find({
where: {
workspaceId: context.workspaceId,
standardId: Not(IsNull()),
},
relations: ['permissionFlags'],
});
const targetStandardRoles = this.standardRoleFactory.create(
@@ -57,10 +62,23 @@ export class WorkspaceSyncRoleService {
'id',
]);
await roleRepository.save({
const createdRole = await roleRepository.save({
...flatRoleData,
workspaceId: context.workspaceId,
});
const roleDefinition = standardRoleDefinitions.find(
(def) => def.standardId === roleToCreate.standardId,
);
if (roleDefinition?.permissionFlags?.length) {
await this.syncPermissionFlags(
permissionFlagRepository,
createdRole.id,
context.workspaceId,
roleDefinition.permissionFlags,
);
}
break;
}
@@ -74,6 +92,19 @@ export class WorkspaceSyncRoleService {
]);
await roleRepository.update({ id: roleToUpdate.id }, flatRoleData);
const roleDefinition = standardRoleDefinitions.find(
(def) => def.standardId === roleToUpdate.standardId,
);
if (roleDefinition?.permissionFlags) {
await this.syncPermissionFlags(
permissionFlagRepository,
roleToUpdate.id,
context.workspaceId,
roleDefinition.permissionFlags,
);
}
break;
}
@@ -86,4 +117,28 @@ export class WorkspaceSyncRoleService {
}
}
}
private async syncPermissionFlags(
permissionFlagRepository: Repository<PermissionFlagEntity>,
roleId: string,
workspaceId: string,
permissionFlags: PermissionFlagType[],
): Promise<void> {
await permissionFlagRepository.delete({
roleId,
workspaceId,
});
if (permissionFlags.length > 0) {
const newPermissionFlags = permissionFlags.map((flag) =>
permissionFlagRepository.create({
roleId,
workspaceId,
flag,
}),
);
await permissionFlagRepository.save(newPermissionFlags);
}
}
}
@@ -1,4 +1,5 @@
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
import { WORKFLOW_MANAGER_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/workflow-manager-role';
export const WORKFLOW_CREATION_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000001',
@@ -44,4 +45,5 @@ Be helpful, thorough, and always prioritize user understanding and workflow effe
modelId: 'auto',
responseFormat: {},
isCustom: false,
standardRoleId: WORKFLOW_MANAGER_ROLE.standardId,
};
@@ -5,4 +5,5 @@ export type StandardAgentDefinition = Omit<
'id' | 'workspaceId' | 'universalIdentifier' | 'standardId'
> & {
standardId: string;
standardRoleId?: string;
};
@@ -1,6 +1,8 @@
import { ADMIN_ROLE } from './roles/admin-role';
import { WORKFLOW_MANAGER_ROLE } from './roles/workflow-manager-role';
import { type StandardRoleDefinition } from './types/standard-role-definition.interface';
export const standardRoleDefinitions = [
ADMIN_ROLE,
WORKFLOW_MANAGER_ROLE,
] as const satisfies StandardRoleDefinition[];
@@ -12,4 +12,7 @@ export const ADMIN_ROLE: StandardRoleDefinition = {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: true,
};
@@ -0,0 +1,20 @@
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface';
export const WORKFLOW_MANAGER_ROLE: StandardRoleDefinition = {
standardId: '20202020-0001-0001-0001-000000000002',
label: 'Workflow Manager',
description: 'Role for managing workflows',
icon: 'IconSettingsAutomation',
isEditable: false,
canUpdateAllSettings: false,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: false,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: false,
permissionFlags: [PermissionFlagType.WORKFLOWS],
};
@@ -1,8 +1,10 @@
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
export type StandardRoleDefinition = Omit<
FlatRole,
'id' | 'workspaceId' | 'universalIdentifier' | 'standardId'
> & {
standardId: string;
permissionFlags?: PermissionFlagType[];
};
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -38,6 +39,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
DataSourceModule,
TypeOrmModule.forFeature([Workspace, FeatureFlag]),
WorkspaceMetadataVersionModule,
AgentRoleModule,
],
providers: [
...workspaceSyncMetadataFactories,