Skip to main content

Roles & Permissions

Krios uses configurable RBAC. Roles are tenant-scoped; users are assigned roles per project; grants compose by (action, resourceType, resourceId).

Default roles

RoleGrants
AdminEvery action × every resource. Cannot be deleted.
Publisherread on resourceType: all, plus create + update + publish + unpublish on resourceType: contentType.
Editorread on resourceType: all, plus create + update on resourceType: contentType.
Viewerread on resourceType: all.

For Publisher, Editor, and Viewer the read grant uses resourceType: "all", while the mutating actions (create / update / publish / unpublish) use resourceType: "contentType".

System roles (isSystem=true) cannot be modified. Custom roles are created via the admin UI or the management API.

Grant shape

{
roleId: string;
action: string; // create | read | update | delete | publish | unpublish |
// manageSchema | manageUsers | manageSettings |
// manageApiKeys | manageRoles | * (all)
resourceType: string; // all | contentType | site | field
resourceId: string?; // null = every resource of that type
effect: "allow" | "deny";
}

Evaluation order: any matching deny wins. Otherwise an allow grant must exist for the (action × resource). Absent grants default to deny.

Field-level permissions (V3)

Setting resourceType="field" + resourceId=fieldDefinitionId gates per-field read / update:

  • No grant → field inherits the entry-level permission (back-compat).
  • read denied → the admin entry editor hides the field. Note: this is enforced only in the admin editor — the delivery GraphQL/REST layers do not currently filter fields by read permission.
  • update denied → the editor disables the field with a 🔒 indicator; PUT requests touching it return 403 field_permission_denied. The write path is the only field-level rule enforced server-side.

Add field-level grants under Settings → Roles → edit role by referencing the field's FieldDefinition.id.

Assigning users

Users hold one role per project via UserProjectRole:

PUT /api/v1/projects/demo/users/{userId}
{ "roleId": "ckl_role_editor" }

A user without a role on a project sees nothing. The admin UI returns "you don't have access to this project" when an unassigned user navigates there.

API key permissions

API keys carry a permissions: string[] array of action keys. Each request the key makes is gated against this list (only when non-empty). Empty array = all actions the key's auth allows. Note that requirePermission short-circuits for API-key auth: API-key requests are not additionally filtered by role grants.

requirePermission helper

In server code:

import { requirePermission } from "@/lib/auth/permissions";

await requirePermission("publish", "contentType", contentType.id);
// throws PermissionError(403) if denied

The helper resolves the caller (session or API key), looks up their effective grants (roles + key permissions), and applies the decision.

Audit

Every grant change is recorded in the audit log: role.created, role.updated, role.deleted. Login attempts (success + lockout), password changes, key creation / revocation are also captured.

Best practices

  • Use the four defaults for most users. Custom roles are powerful but make access decisions harder to reason about.
  • Don't grant publish to humans by default. Pair with workflows so authors submit for review and publishers approve.
  • Field-level permissions are surgical. Reach for them only when entry-level granularity isn't enough — most access concerns are solved at the entry / contentType layer.
  • Rotate API keys. Set expiresAt on long-lived keys, especially preview keys.