Zornux docs
Get started Spec

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 …:

zornux
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."
RequirementHolds when…
require authenticationThe 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 OtherAnother (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:

zornux
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:

zornux
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.

OAuth scopes

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:

zornux
import auth

policy CanReadReports
    check
        give back auth.has_scope(user, "reports.read")
    end
end
Everything fails closed

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?"):

zornux
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

zornux
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
401 vs 403 — let auth.deny decide

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.

Denials are logged, not thrown

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 ZX2900ZX2999. Next: work that runs outside the request — Background Jobs.