Roles & Permissions
Krios uses configurable RBAC. Roles are tenant-scoped; users are assigned roles per project; grants compose by (action, resourceType, resourceId).
Default roles
| Role | Grants |
|---|---|
| Admin | Every action × every resource. Cannot be deleted. |
| Publisher | read on resourceType: all, plus create + update + publish + unpublish on resourceType: contentType. |
| Editor | read on resourceType: all, plus create + update on resourceType: contentType. |
| Viewer | read 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).
readdenied → 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.updatedenied → the editor disables the field with a 🔒 indicator; PUT requests touching it return403 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
publishto 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
expiresAton long-lived keys, especially preview keys.