Quickstart
Run the example leave service, see each kind of check at work, then add a permission end to end.
The repo ships the same small leave-request service twice: examples/go-app (net/http and database/sql) and examples/nest-app (NestJS and Drizzle). Both pass the same end-to-end test. This page uses the Go one.
Run it
You need Docker, Go and the zaccess CLI (go install github.com/zynactra/access/go/cmd/zaccess@latest).
git clone https://github.com/zynactra/access.git && cd access/examples/go-app
docker compose up -d # Postgres, and Cerbos serving ./access/generated/policies
zaccess test # the policy tests pass
DATABASE_URL='postgres://postgres@localhost:5432/postgres?sslmode=disable' go run . -seed-seed creates an org called demo with a reporting line and an org unit tree:
u_ceo ── u_mgr ── u_emp hq ── fin ── fin_ap
│ └── u_1 └── it
├── u_peer
└── u_hrEveryone is an employee, u_mgr is a line_manager for their team, and u_hr is an hr_officer for fin and everything below it. The service reads the caller from an X-Demo-User header; a real product verifies a token instead.
Try the three checks
The route guard. Someone who holds a permission nowhere is turned away before the handler runs:
curl -i -X POST -H 'X-Demo-User: u_peer' localhost:8080/leave-requests/anything/approve # 403The record check. The employee asks for leave and their manager approves it. Approving it a second time fails: the manager holds the permission for this record, but the condition says only pending requests:
curl -H 'X-Demo-User: u_emp' -d '{"days":2,"reason":"family"}' localhost:8080/leave-requests
# {"id":"<id>","status":"pending",...}
curl -X POST -H 'X-Demo-User: u_mgr' localhost:8080/leave-requests/<id>/approve # approved
curl -i -X POST -H 'X-Demo-User: u_mgr' localhost:8080/leave-requests/<id>/approve # 403The list filter. Each person sees exactly the requests they may read, filtered in SQL:
curl -H 'X-Demo-User: u_mgr' localhost:8080/leave-requests # their team's requests
curl -H 'X-Demo-User: u_peer' localhost:8080/leave-requests # only their ownWhy? The access API explains any decision:
curl -H 'X-Demo-User: u_mgr' -d '{"permission":"leave.request:approve","id":"<id>"}' localhost:8080/access/explain
# {"allowed":false,"reason":"condition","grants":[{"source":"assignment","roleKey":"line_manager","scope":"team",...}],...}Add a permission end to end
Say managers should be able to cancel approved leave in their team.
Declare it
In access/plugins/leave/access.yaml, add cancelled to the status enum (checks reject values outside it), and the action with the scopes it may be granted at and its condition:
status:
enum: [pending, approved, rejected, cancelled]
actions:
cancel:
scopes: [team, org_unit, global]
description: Cancel approved leave
condition: R.attr.status == "approved"Give it to the roles that should have it, in access/access.yaml:
line_manager:
permissions:
leave.request:cancel: inherit # at the assignment's scope (their team)Generate and test the policy
zaccess gen
zaccess test # fails: the new condition has no policy test yetAdd cases to policy-tests/leave_request_test.yaml: allowed for an approved request in the team, denied for a pending one, denied outside the team, denied in another org. Run zaccess test again. See Testing.
Enforce it
Guard the route with the generated constant, load the record, and assert:
mux.Handle("POST /leave-requests/{id}/cancel", g.Require(accessgen.LeaveRequestCancel, http.HandlerFunc(a.cancel)))
func (a *App) cancel(w http.ResponseWriter, r *http.Request) {
s, _ := accesshttp.SubjectFrom(r.Context())
l, err := a.loadLeave(r.Context(), s.OrgID, r.PathValue("id"))
if err == nil && l == nil {
err = &access.Error{Code: access.CodeNotFound, Message: "leave request not found"}
}
if err == nil {
err = a.Access.Assert(r.Context(), s, accessgen.LeaveRequestCancel, l.resource())
}
if err != nil {
accesshttp.WriteError(w, err) // 404, 403, or 503 when Cerbos is down
return
}
// update leave_requests set status = 'cancelled' where id = $1 and status = 'approved'
}Strict mode would fail this route with 500 unchecked_requirement if the Assert were missing.
Sync the catalog
The service's startup SyncCatalog (or zaccess sync) adds the permission to the database's catalog and updates the system roles, so admins see it in the role editor.
That's the whole loop: declare, generate, test, enforce. Next, read How it works, or go straight to the guide for your stack: Go, NestJS.