Skip to main content

Authorization

Role-based access control

Role + PermissionGrant — roles are tenant-scoped; users assume a role per project (UserProjectRole). See Roles & Permissions for the full surface.

Default roles:

RoleGrants
Adminevery action × every resource
Publisherread + create + update + publish on contentType:all
Editorread + create + update on contentType:all
Viewerread on contentType:all

requirePermission(action, resourceType?, resourceId?) runs at the top of every mutation route and throws 403 forbidden on failure.

Grant evaluation

function check(grants, action, resourceType, resourceId): "allow" | "deny" {
let allowed = false;
for (const g of grants) {
if (g.action !== action && g.action !== "*") continue;
if (g.resourceType !== resourceType && g.resourceType !== "all") continue;
if (g.resourceId !== null && g.resourceId !== resourceId) continue;
if (g.effect === "deny") return "deny"; // deny short-circuits
if (g.effect === "allow") allowed = true;
}
return allowed ? "allow" : "deny"; // default deny
}

Any matching deny wins. Otherwise an allow must exist for the (action × resource). Absent grants default to deny.

Field-level permissions (V3)

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

  • No grant → field inherits the entry-level permission (back-compat).
  • read denied → the entry editor hides the field; REST/GraphQL omit it.
  • update denied → the editor disables the field with a 🔒 indicator; PUT requests touching it return 403 field_permission_denied with details.restricted[].

API key scoping

Three orthogonal scopes:

  • Type (delivery / preview / management) — what kinds of operations the key can perform.
  • siteId — restricts the key to operations against one site. Cross-site requests respond 404 (not 403) to avoid leaking which site slugs exist.
  • environmentId (V2) — restricts the key to one environment. Mismatched X-Krios-Environment hint returns 403 environment_scope_mismatch.

Keys also carry a permissions: string[] array — additional action restrictions on top of the role-based gate.

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.

Enforcement points

LayerWhat it gates
Edge middlewareCORS, rate limit per IP, request-id propagation. Doesn't read DB.
authenticateRequestResolves the caller (session or API key).
resolveProjectBySlugProject lookup + IP allowlist gate (V3).
requirePermissionPer-action grant check.
validateFieldValueField-level write validation (including custom:* types).
assertReadyToPublishRequired-reference gate at publish.

Each layer fails closed. A request that gets to the route handler has already passed every prior gate.

Errors

Production errors return { error: code, message, details? } with no stack traces, schema fragments, or cross-tenant data. Codes are machine-readable; messages are operator-friendly.

src/lib/auth/http.ts:ApiError is the shared error class — every route uses it via errorResponse(e).