5d88cf7f2f
Adds role management tools to the AI chat so the agent can create and configure roles, including row-level permissions. ## What A `RoleToolProvider` in `tool-provider/providers/`, mirroring `webhook-tool.provider.ts`, registered in `tool-provider.module.ts` and gated behind `PermissionFlagType.ROLES` via `PermissionsService.checkRolesPermissions` (same pattern as the VIEWS/WORKFLOWS gating). Tools: - `list_roles` — global record permissions, settings access, per-object overrides, permission flags, assignability; optionally includes row-level rules - `create_role`, `update_role`, `delete_role` - `assign_role_to_workspace_member` — via `UserRoleService.assignRoleToManyUserWorkspace`, which goes through role-target - `upsert_object_permissions` — per-object overrides, e.g. read-only on a given object - `upsert_row_level_permission_rules` — reuses `RowLevelPermissionPredicateService.upsertRowLevelPermissionPredicates` and the predicate-group service, so the agent can express rules like "members with this role only see records where the owner field matches the current user" (a predicate with `workspaceMemberFieldMetadataId` pointing at the workspaceMember `id` field, resolved to the current user at query time) Everything routes through the existing role services and DTOs (`RoleService`, `ObjectPermissionService`, `UserRoleService`, the row-level predicate services) rather than reimplementing them. A new `ToolCategory.ROLE` is added to `twenty-shared`, along with its label in the exhaustive switch in `build-tool-catalog-section.util.ts`. ## Safeguards - Any mutation on a role with `isEditable: false` is rejected. That covers the Admin role, which is created non-editable, and matches what Settings blocks. - Deleting the role the caller is currently acting under is rejected, since deletion would rebind them to the workspace default role. - Setting `canUpdateAllSettings: false` on the caller's own role is rejected unless that role keeps an explicit ROLES permission flag. - Changing your own role via `assign_role_to_workspace_member` is rejected, checked both by workspace member id and by resolved user workspace id. The tool-layer checks are deliberate pre-checks: the migration validators and services enforce the same rules downstream (`validate-role-is-editable.util.ts`, default-role deletion, last-admin unassignment, write-without-read consistency), but catching them early gives the model a named, actionable message instead of a build failure report. Where the deeper layer does reject, `formatValidationErrors` expands the migration exception so the underlying per-entity errors reach the model rather than a generic summary. Worth flagging for reviewers: the self-lockout protection currently lives only at the tool layer. A human admin can still strip settings access from their own role through Settings/GraphQL. Closing that would mean changing `RoleService`/`UserRoleService` behavior for the human path, which felt like a separate decision than what this change is scoped to. ## Notes `ToolCategory.ROLE` is intentionally left out of `WORKFLOW_AGENT_REGISTRY_TOOL_CATEGORIES`, so workflow agents don't get these tools; only the chat surface and the MCP/tool-index paths that share the registry do. ## Testing - 24 unit tests in `providers/__tests__/role-tool.provider.spec.ts`, covering permission gating, descriptor exposure, each safeguard, the N+1-free list path, and validation-error surfacing - 333 tests pass across the tool-provider, role, object-permission and ai suites - `npx nx lint:diff-with-main twenty-server` and `npx nx typecheck twenty-server` are clean --- _Generated by [Claude Code](https://claude.ai/code/session_0131sLKVsRuaDoaKCxFM8g4Z)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23613?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
66 lines
3.5 KiB
TypeScript
66 lines
3.5 KiB
TypeScript
import { assertUnreachable } from 'twenty-shared/utils';
|
|
|
|
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
|
|
|
export const permissionRestApiExceptionCodeToHttpStatus = (
|
|
code: PermissionsExceptionCode,
|
|
): number => {
|
|
switch (code) {
|
|
case PermissionsExceptionCode.PERMISSION_DENIED:
|
|
case PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT:
|
|
case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS:
|
|
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
|
case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE:
|
|
case PermissionsExceptionCode.CANNOT_DELETE_OWN_ROLE:
|
|
case PermissionsExceptionCode.CANNOT_REVOKE_OWN_SETTINGS_ACCESS:
|
|
case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER:
|
|
case PermissionsExceptionCode.ROLE_NOT_EDITABLE:
|
|
case PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT:
|
|
case PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT:
|
|
return 403;
|
|
case PermissionsExceptionCode.INVALID_ARG:
|
|
case PermissionsExceptionCode.INVALID_SETTING:
|
|
case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT:
|
|
case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION:
|
|
case PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED:
|
|
case PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT:
|
|
case PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT:
|
|
case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED:
|
|
case PermissionsExceptionCode.ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET:
|
|
case PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS:
|
|
case PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS:
|
|
case PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
|
return 400;
|
|
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
|
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
|
case PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND:
|
|
case PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND:
|
|
case PermissionsExceptionCode.PERMISSION_NOT_FOUND:
|
|
return 404;
|
|
case PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED:
|
|
case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND:
|
|
case PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH:
|
|
case PermissionsExceptionCode.TOO_MANY_ADMIN_CANDIDATES:
|
|
case PermissionsExceptionCode.USER_WORKSPACE_ALREADY_HAS_ROLE:
|
|
case PermissionsExceptionCode.ADMIN_ROLE_NOT_FOUND:
|
|
case PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED:
|
|
case PermissionsExceptionCode.WORKSPACE_MEMBER_NOT_FOUND:
|
|
case PermissionsExceptionCode.UNKNOWN_OPERATION_NAME:
|
|
case PermissionsExceptionCode.UNKNOWN_REQUIRED_PERMISSION:
|
|
case PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE:
|
|
case PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE:
|
|
case PermissionsExceptionCode.METHOD_NOT_ALLOWED:
|
|
case PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED:
|
|
case PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND:
|
|
case PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND:
|
|
case PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED:
|
|
case PermissionsExceptionCode.COMPOSITE_TYPE_NOT_FOUND:
|
|
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
|
case PermissionsExceptionCode.APPLICATION_ROLE_NOT_FOUND:
|
|
case PermissionsExceptionCode.ROLE_BELONGS_TO_ANOTHER_APPLICATION:
|
|
return 500;
|
|
default:
|
|
return assertUnreachable(code);
|
|
}
|
|
};
|