Skip to content

Permissions

The Callora host authorizes every operator/admin API call against a fixed set of RBAC permission keys. This page catalogues all 37 permission keys — the key string, its C# constant, and the endpoints that require it — and explains how the RBAC model, the callora_scope claim, and the SuperAdmin bypass fit together.

The RBAC model

Authorization has two orthogonal parts: scope (how far a session reaches) and permissions (which actions it may perform).

  • Scope is the callora_scope claim (BackendClaimTypes.CalloraScope), stamped at token issuance. Its two tiers are platform and workspace. A platform-scoped session (a platform operator) reaches across every workspace; a workspace-scoped session is bound to its workspace_key claim and can only touch that workspace. A principal without the scope claim never gains platform-wide access (fail-closed).
  • Permissions are permission claims (BackendClaimTypes.Permission) carrying the keys below. An endpoint guarded by RequirePermission(...) allows the call only if the caller holds the matching key (or the * wildcard).

Scope is reach, not authority

Being a platform operator grants scope, not blanket authority. Only the SuperAdmin role satisfies permission checks unconditionally. Any other operator role (e.g. an API-key host.api role, or a custom operator role) still draws its concrete rights from its permission grants — an operator role with platform scope but no permission grants reaches every workspace yet is denied every permission-gated action (403).

Roles

Role names live in Callora.Core.Application.Security.BackendRoles:

Role constantValueScopeNotes
BackendRoles.SuperAdminsuperadminPlatformUnrestricted global backend access. The only role that satisfies permission checks unconditionally. Seeded with the * wildcard grant.
BackendRoles.AdminadminWorkspaceWorkspace administrator — not a global operator; grants are carried per workspace via WorkspaceMembership.
BackendRoles.HostApihost.apiPlatformRole for API-key host access; treated as a platform operator (scope, not authority).

Global identity vs. workspace membership

user.* write keys govern the global BackendUser — credentials, account erasure and the data-subject export. Those operations reach every workspace the subject belongs to, so the endpoints additionally require platform scope: a workspace-scoped session is rejected with 403 even while holding the key.

Workspace administration uses membership.* instead, which manages only who belongs to a workspace and in which workspace role. A workspace-bound caller reaches only its own workspace; any other {workspaceKey} answers 404. WorkspaceRolePermissions therefore grants the workspace admin role membership.read/update/delete and user.read, never a user.* write key.

SuperAdmin bypass and seeding

BackendRbacDatabaseSeeder (in Callora.Core.Infrastructure.Persistence) ensures the superadmin role exists as a system role (IsSystem = true) holding a single grant with PermissionKey = "*". The permission check (Callora.Core.Infrastructure.Security.EndpointAuthorizationExtensions) short-circuits to allow when user.IsInRole(BackendRoles.SuperAdmin); otherwise it requires a matching permission (or scope) claim, accepting either an exact key match or *.

The seeder also assigns the demo admin user and the one-time bootstrap operator (InitialOperator) to the superadmin role. Non-SuperAdmin roles derive their permissions from BackendHostOptions.RbacRoles via BackendRbacPermissionCatalog.

The permission-key catalogue

All keys are defined as public const string in Callora.Core.Application.Security.BackendPermissionKeys. The table below lists every key, its constant, and the endpoint(s) that enforce it — via the minimal-API RequirePermission(...) filter or the equivalent [CalloraPermission(...)] attribute on MVC controllers.

Permission keyC# constantAuthorizes (endpoints)
tenant.createTenantCreatePOST /api/tenants
tenant.readTenantReadGET /api/tenants, GET /api/tenants/{tenantKey}
tenant.updateTenantUpdatePOST /api/tenants/{tenantKey}/activate, POST /api/tenants/{tenantKey}/suspend
tenant.deleteTenantDeleteDELETE /api/tenants/{tenantKey}
plugin.createPluginCreatePOST /api/plugins/install, /install/local, /install/nuget, POST /api/plugins/{id}/update/nuget, /update/local
plugin.readPluginReadGET /api/plugins, /installed, /signature-report, /audit, /contracts/support, /contracts/compatibility, /security/trusted-signers, /workspaces/{wk}/entitlements/{pid}, /tenants/{tid}/entitlements/{pid}; GET /api/entitlements
plugin.deletePluginDeleteDELETE /api/plugins/{id}
plugin.executePluginExecutePOST /api/plugins/{id}/activate, /deactivate; PUT /api/entitlements; POST /api/entitlements/sync
config.readConfigReadGET /api/config/definitions, GET /api/config/effective
config.updateConfigUpdatePUT /api/config/values
webhook.readWebhookReadGET /api/webhooks
webhook.manageWebhookManagePOST /api/webhooks, PUT /api/webhooks/{id}/activation, DELETE /api/webhooks/{id}
notification.readNotificationReadGET /api/notifications, PUT /api/notifications/{id}/read
media.readMediaReadGET /api/media, GET /api/media/{id}/content
media.manageMediaManagePOST /api/media, DELETE /api/media/{id}
customfield.readCustomFieldReadGET /api/custom-fields/definitions, GET /api/custom-fields/{entityName}/{entityId}
customfield.updateCustomFieldUpdatePUT /api/custom-fields/{entityName}/{entityId}
flow.readFlowReadGET /api/flows; GET /api/events/catalog
flow.manageFlowManagePOST /api/flows, PUT /api/flows/{id}, DELETE /api/flows/{id}
job.readJobReadGET /api/jobs
extension.readExtensionReadGET /api/themes/definitions, /workspaces/{wk}, /workspaces/{wk}/effective, /workspaces/{wk}/settings; GET /workspace/themes/effective
extension.updateExtensionUpdatePUT /api/themes/definitions/{templateKey}/plugins/{pluginId}/versions/{version} (and its /activation), PUT /api/themes/workspaces/{wk}, DELETE /api/themes/workspaces/{wk}, PUT /api/themes/workspaces/{wk}/settings
role.readRoleReadGET /api/rbac/roles, /permissions, /users
role.updateRoleUpdatePUT /api/rbac/roles/{role}, DELETE /api/rbac/roles/{role}, PUT /api/rbac/users/{userId}, DELETE /api/rbac/users/{userId}
user.createUserCreatePOST /api/usersplatform operators only
user.readUserReadGET /api/users, GET /api/users/{userId} (workspace-filtered), GET /api/users/{userId}/data-export (platform operators only)
user.updateUserUpdatePUT /api/users/{userId}, PUT /api/users/{userId}/activationplatform operators only
user.deleteUserDeleteDELETE /api/users/{userId}platform operators only
membership.readMembershipReadGET /api/workspaces/{wk}/members
membership.updateMembershipUpdatePUT /api/workspaces/{wk}/members/{userId}
membership.deleteMembershipDeleteDELETE /api/workspaces/{wk}/members/{userId}
workspace.createWorkspaceCreate(defined; no endpoint currently enforces it)
workspace.readWorkspaceReadGET /api/workspaces, /{wk}, /{wk}/members; GET /api/surfaces, /{surfaceKey}
workspace.updateWorkspaceUpdatePUT /api/workspaces/{wk}, PUT /api/workspaces/{wk}/members/{userId}, DELETE /api/workspaces/{wk}/members/{userId}; PUT /api/surfaces/{surfaceKey}, DELETE /api/surfaces/{surfaceKey}
workspace.deleteWorkspaceDeleteDELETE /api/workspaces/{wk}
integration.readIntegrationReadGET /api/security/integrations
integration.manageIntegrationManagePOST /api/security/integrations, DELETE /api/security/integrations/{id}

Key naming

Keys follow a {function}.{action} shape where the action is one of create / read / update / delete / execute (BackendPermissionActions). Custom operator roles built from BackendHostOptions.RbacRoles compose their grants from the same catalogue.

Status: workspace.create is a defined constant but is not yet enforced by any endpoint — workspaces are created through other flows today. It is reserved for a future dedicated endpoint.

Plugin controllers use the same keys

A plugin that contributes backend routes annotates them with [CalloraRoute(..., Permission = "…")]. The Permission string is one of the same keys above (or a plugin-defined key). At dispatch, the host (PluginApiEndpointDataSource) enforces it: the caller must hold a matching permission claim (or *), otherwise the request is rejected with 403 and an RFC 9457 problem response. An empty Permission means authenticated-only.

csharp
[CalloraRoute("POST", "/dialer/campaigns", Permission = "plugin.execute")]
public Task<IResult> StartCampaign(...) { ... }

Plugin-route bypass differs

Plugin-route permission enforcement checks the permission claim (or *) directly. Unlike the host's RequirePermission(...) extension, it does not special-case the SuperAdmin role — a SuperAdmin passes because the seeded * grant is stamped as a permission claim, not because of a role short-circuit.

See also

  • REST API — the full endpoint catalogue these permissions guard.
  • Backend extensions — contributing controllers with [CalloraRoute] and declaring their required permission.
  • Architecture — where RBAC sits in the host.