0007 — Cedar for authorization
Authorization is evaluated in-process from Cedar policy files.
Decided. Each service authorizes its own requests using Cedar policies, held as
.cedar files, compiled at startup, evaluated in process. The edge authenticates;
the service authorizes.
Superseded in part by 0013. Authentication (JWT validation) moves into each service; Cedar evaluation in process is unchanged.
Superseded in part by 0047 (Ory Kratos and Hydra for authentication; identity stays first-party, docs/decisions/0047-ory-for-authentication-identity-stays-first-party.md; not yet mirrored here). The local copy is
principals,principal_grants(explicit scoped grants), andendpoint_access, notprincipal_rolesandprincipal_groups. Which roles may call an action moves from policy files intoendpoint_access, so that part no longer needs a release; the action and its scoping still do. Evaluation in process is unchanged.
Alternatives
- Hand-written checks in Go. Simpler to read, worse to audit, and impossible to present as a coherent list of who may do what.
- Authorization at the gateway. Puts domain knowledge in the wrong place and makes every service reachable-and-unprotected from inside.
- OPA / Rego. Comparable capability; more operational surface if run as a sidecar, and a less pleasant language for this shape of rule.
Why
Cedar policies are data with a defined evaluation semantics: default deny, explicit
permits, forbid overriding, and a diagnostic explaining which policy decided. It
costs no infrastructure — no sidecar, no network call — and it already exists in the
legacy system, so the policies and the team's familiarity carry over.
The decisive property for our purposes: because policies are parseable files, the generator can extract them and put the required action and its condition into the OpenAPI document, the handler comment, and a generated authorization matrix. An engineer sees what an endpoint requires without reading handler code.
Why local evaluation with replicated identity is not an accident
This is a well-established pattern: a local Policy Decision Point (PDP) with a replicated identity store. The alternative — calling a central authorization service per request — makes that service a hard dependency of every service on every request: its latency adds to everything, and its downtime takes the whole platform down with it.
Cedar was designed specifically for this model. The Cedar engine evaluates policies in-process against a local entity store. The design expectation is that you bring identity data to the evaluator, not the other way around. AWS built Cedar for exactly this topology.
In practice: each service keeps a local copy of principals, principal_roles, and
principal_groups, refreshed from the identity service every ~60s in a single
replacing transaction. Authorization is a local SQL read in the same transaction as
the command — no network hop, no new failure mode, no window where a disabled
principal slips through between the auth check and the write.
The cost is revocation lag bounded to the refresh interval. That is a deliberate, named tradeoff, not an oversight.
How it is wired
- The entity spec names a
rule(a Cedar policy id) per command and query. The action name is conventional:<service>::<Command>. authz: noneis allowed but requires areason:.- Policies carry
@idand@docannotations; both end up in the docs. - The generator reads
resource.*references out of the policy condition and emits the handler code that supplies those attributes.
Consequences
- Adding people, buildings, and group memberships is data, no release. Adding a new command or a new kind of rule needs a release, because the action must be listed in a policy.
- Ownership rules need attribute-bearing resource entities, not a bare type and id —
CheckWithResourcerather thanCheck. It arrives with the first scoping rule; the chart of accounts only needs role-based checks. - Policy files are a reviewable security surface. Every policy needs at least one allow and one deny test.