Troubleshooting
Symptoms, their causes and the fix, from unchecked requirements to stale caches.
Start with Explain: POST /access/explain (or the Explain tab in the admin screens) shows a person's grants for a permission and why a record was allowed or denied. Most questions end there.
| Symptom | Cause | Fix |
|---|---|---|
500 unchecked_requirement | A Require / @RequirePermission route returned success without checking that permission against a record. | Add Assert/assert (or Plan for lists) for the same permission the route declares. Checking a different permission does not count. |
403 with reason undeclared_route (Nest) | The route has no access decorator. | Add @RequirePermission, @RequireCapability, or @AccessPublic() for truly public routes. |
Denied with reason invalid_resource (400 invalid in strict mode) | The resource is missing a declared attribute, has an undeclared one, a wrong type, a value outside an enum, or the wrong kind. | Build resources in one function per kind with every attribute from the manifest plus orgId. Numbers must be numbers (cast numeric columns). |
Denied with reason unknown_permission | Typo or a permission not in the manifest (or generated files are stale). | Use generated constants; run zaccess gen. |
Denied with reason no_grant | The user holds the permission at no scope. | Check their assignments (the Assignments tab, or GET /access/assignments?userId=) or use POST /access/explain. |
Explain says scope | They hold it, but the record is outside every scope they hold it at. | Check the record's ownerPath/orgUnitId against their grants (listed in the explanation). |
Explain says condition | The record is in scope, but the action's condition failed (for example the request isn't pending, or it's their own). | Expected; the condition is in the manifest. |
Explain says other_org | The record's orgId isn't the subject's org. | Load the record in the subject's org, or check how orgId is set. |
Explain says org_rule | An org rule for the subject's org denied it; orgRule names it. | Expected for that org. Change the rule in orgs/<org>.yaml if it's wrong. |
Denied with reason policy | Cerbos denied this record. The check doesn't say which part of the rule failed. | POST /access/explain for that person, permission and record: it answers scope, condition, other_org or org_rule. If Explain itself says policy, regenerate with zaccess gen (policies from before outputs existed) and check the condition for attribute errors. |
| Every check for one org is denied | The manifest has org rules for it but Cerbos lacks the scoped policy (stale orgs/ folder). | Deploy the generated policies/orgs/ with the manifest. |
team grants do nothing | The resolver returned no reportingPath for the user, so prefix grants are switched off. | Fix PrincipalAttrs; paths must start and end with /. |
org_unit grant misses child units | expand: true is not set, or ExpandSet does not return descendants. | Set expand and return the IDs plus everything below them. |
503 unavailable | Cerbos, the store or the resolver failed (timeouts default to 250 ms, one retry). Every such error is a denial. | Check the sidecar and the database. Do not add an allow fallback. |
sqlfilter: operator "x" is not supported | A condition uses an operator the SQL translator refuses (to avoid widening a query). | Rewrite the condition with ==, !=, <, <=, >, >=, in, startsWith/endsWith/contains, &&, ||, !, or keep that rule out of list filters. |
sqlfilter: no column mapped for attribute "x" | The columns map lacks an attribute the plan uses. | Map every declared attribute that policies can reference. |
escalation from the admin API | The caller cannot grant that permission at that scope (or is assigning to themselves). | Expected. Grant from someone whose reach covers it, or narrow the scope. |
invalid: "a reason is required" | Sensitive permission granted globally, or a break-glass role. | Send reason; break-glass also needs validUntil within 24 hours. |
conflict on revoke | Last active access_admin, or already revoked. | Assign another admin first. |
| Changes take a minute to apply | The NOTIFY listener is not running on that instance. | Start pqlisten.Listen / listen(); otherwise the cache TTL (60 s) bounds staleness. |
zaccess check -verify fails in CI | Generated files are stale or hand-edited. | Run zaccess gen and commit the result. Never edit generated files. |
zaccess test says an action has no test | An action with a condition has no policy test. | Add allow and deny cases for it. |