Web & APIs
Authorization
Beyond simple role checks, a policy names a set of
requirements and enforces them declaratively. It extends
the restrict guard you already know —
one English vocabulary, never a parallel dialect — and it
fails closed.
Declaring a policy
Every require line must hold; or offers
alternatives within a line. Enforce it with the familiar guard,
restrict to policy Name otherwise …:
policy CanManageOrders
require authentication
require role "Manager" or "Owner"
end
restrict to policy CanManageOrders otherwise show "Access denied — managers only."
show "Continuing with public work."
| Requirement | Holds when… |
|---|---|
require authentication | The principal is signed in. |
require role "…" or "…" | The principal has one of the roles. |
require permission "…" | The principal holds the permission. |
require claim "name" is "value" | A claim on the principal matches. |
require policy Other | Another (parameterless) policy grants. |
Claims
Claims are named facts about the principal (department, tenant, plan…),
supplied by the host — on the CLI with --claim name=value, or
by an authentication provider. A policy can require them, and the
claim("name") built-in reads the live principal's claim
anywhere:
policy SalesOnly
require authentication
require claim "department" is "sales" or "support"
end
Custom decisions: check blocks
When a rule needs real logic, a check … end block decides with
ordinary Zornux code, giving back a truth. The current principal is the
injected user value:
policy NamedUser
check
give back user.is_authenticated
end
end
user exposes .name,
.is_authenticated, .roles,
.permissions, and .claims. Declarative
require lines run first, then the check.
Alongside roles, permissions, and claims, authorization also spans OAuth scopes. A scope isn't a declarative require form — check it inside a check block with auth.has_scope:
import auth
policy CanReadReports
check
give back auth.has_scope(user, "reports.read")
end
end
If a check crashes, falls off the end, or gives back a non-truth, access is denied. Authorization never fails open — a bug can't accidentally grant access.
Resource-based policies
A policy can take the resource it protects. Declare with order
and the guard passes it — so a check can compare the resource to the user
(e.g. "is this the owner?"):
policy CanEditOrder
with order
check
give back order.owner is user.name
end
end
restrict to policy CanEditOrder with order otherwise give back status 403
In a controller
controller Reports at "/reports"
on GET "/" with Request request
restrict to policy SalesOnly otherwise give back auth.deny(request)
give back ok message "Q3 sales report"
end
end
A hard-coded give back status 403 tells an unauthenticated caller they're forbidden, when the correct answer is 401 Unauthorized. auth.deny(request) returns 403 when the caller is authenticated but lacks the authority, and 401 when they're anonymous — the right status without branching yourself.
A denied guard automatically emits a warning event (category security, with target, kind, actor, and correlation id) into the log stream; a granted guard emits nothing. A denial is never a diagnostic — it runs the otherwise branch.
One new keyword, policy. Diagnostics are ZX2900–ZX2999. Next: work that runs outside the request — Background Jobs.