Categorias
Programming Segurança

Permissions and Authorisation: Implementing with Cedar

Disclaimer: This is the point-in-time version of my "Playbook - Permissions and Authorisation: Cedar Implementation", which I will keep up-to-date. This article is the sequel to "Permissions and Authorisation: A Practical Playbook", which covers the domain model, enforcement boundaries, and mechanism selection. Read that first. This article assumes the model is built and a policy engine is warranted.

The previous article ended with a guidance: introduce a policy engine when the model combines roles with ownership, relationships, tenant containment, and attributes; when the same decision must be made in several services; or when policy review, simulation, traceability, or controlled change is a real requirement. Cedar is one concrete option when that point is reached. This article walks through how to apply the playbook's model using Cedar specifically and where Cedar stops being the answer.

What Cedar is

Cedar is a policy language and decision engine developed by AWS and released open-source under Apache 2.0. It evaluates an authorisation request with four parts, being principal, action, resource, context (called PARC), plus a set of entities representing trusted domain facts and their relationships. It returns Allow or Deny and decision diagnostics.

Cedar's evaluation semantics provide three useful safety properties:

  1. Default deny. No matching permit means Deny. There is no implicit allow.
  2. Forbid wins. A matching forbid overrides any matching permit. Guardrails are expressible as first-class policy, not afterthoughts.
  3. Errors skip the affected policy. A policy evaluation error causes that policy to be skipped and reported in diagnostics rather than automatically determining the entire decision. The request continues to be evaluated against remaining policies.

The third rule makes validation and diagnostics operationally important. A faulty policy does not itself cause a global outage, but it can be skipped while another permit still allows a request that the faulty policy was meant to restrict. Validate policies against the schema before publication, alert on evaluation diagnostics, and test both policy and schema changes.

A Cedar design normally contains:

  • a schema for entity types, attributes, memberships, actions, and context shapes;
  • entities representing trusted domain facts and relationships;
  • permit and forbid policies;
  • a policy store and change process;
  • an application adapter that constructs the PARC request and relevant entity slice, invokes Cedar, records the decision, and enforces it.

What Cedar does not solve

Cedar is a decision engine, not a complete authorisation system. This is pivotal to understand what is not Cedar's responsibility. It does not:

  • authenticate users or issue/validate JWTs;
  • decide which entity data is true: your application must assemble trusted, correctly scoped entity data;
  • filter a database query automatically or protect a storage bucket on its own;
  • replace validation of input, tenancy checks, audit retention, rate limits, or business invariants;
  • provide a complete UI, approval workflow, or governance process for users editing policies.

If policy authors are customers or non-engineering staff, design policy administration as a product: constrained templates, preview/simulation, approval and rollback, audit history, and strict meta-permissions for who may grant which access.

When Cedar is a good fit

Cedar is particularly strong when a system needs combinations of RBAC, ownership/relationship rules, tenant containment, and attributes; when decisions are shared across services; or when policy review and explainability matter.

Use groups to represent broad roles and policies to grant role capabilities. Add resource relationships or attributes for rules that roles cannot express cleanly. Keep individual grants and exceptions narrow, scoped to both principal and resource where possible. Prefer fine-grained permissions in the model, then aggregate them into user-facing product roles or screens.

Cedar is not the only policy engine, and it is not always the right one. If your rules are few, stable, and local to one service, centrally tested application code is simpler and clearer. If you need a full relationship-based access control system with deep graph traversal, consider whether Cedar's entity model is expressive enough for your relationship depth or whether a dedicated ReBAC system is more appropriate. Choose based on the behaviour the system needs, not the novelty of the tool.

Implementing Cedar: a step-by-step approach

1. Start with one high-value boundary

Choose one coherent domain, such as organisation membership, documents, or projects, not the entire legacy permission surface. Keep existing authorisation in place elsewhere. Define the success measure: for example, all document operations are evaluated at one server-side boundary and the decision can be explained from logs.

This mirrors the playbook's general guidance: centralise decisions early enough that the model can evolve deliberately, rather than attempting a big-bang migration of every permission surface at once.

2. Define the Cedar schema and action vocabulary first

Define the namespace, entity types, action groups, and action applicability before drafting many policies. Ensure each action identifies the permitted principal and resource types and the specific transient context shape it accepts.

namespace ExampleApp {
  entity User in [Role, Organisation] {};
  entity Role {};
  entity Organisation {};
  entity Document in [Organisation] = { owner: User, locked: Bool };

  action "document.read" appliesTo {
    principal: [User],
    resource: [Document],
    context: {}
  };

  action "document.update" appliesTo {
    principal: [User],
    resource: [Document],
    context: {}
  };
}

This is illustrative only. The schema must reflect the actual domain and its authoritative data sources. The Cedar schema defines what an authorisation request and its entities are allowed to look like; it does not establish where those facts come from or whether they are trustworthy. That is the responsibility of the adapter and the data contract described in step 5. The action vocabulary is where the playbook's guidance on business actions applies directly: use document.read, document.update, invoice.approve — not HTTP verbs or UI labels. Make the resource explicit even for create and list operations, where the resource is normally the destination container or tenant.

3. Model policy rules as business rules

Policies should be specific and readable. A role grant and an owner rule may coexist; a forbid can express a guardrail that must win even if a role policy permits access.

// Role-based: document readers can read any document in their scope.
// Assumes the user's role membership is represented in the entity graph.
permit (
  principal in ExampleApp::Role::"document_reader",
  action == ExampleApp::Action::"document.read",
  resource
);

// Ownership-based: owners can update their own unlocked documents
permit (
  principal,
  action == ExampleApp::Action::"document.update",
  resource
)
when { resource.owner == principal && !resource.locked };

// Guardrail: locked documents cannot be updated, even by the owner
forbid (
  principal,
  action == ExampleApp::Action::"document.update",
  resource
)
when { resource.locked };

Do not paste this example into production unchanged. In particular, scope policies as tightly as the domain requires and validate all policy text against the deployed schema.

The forbid-wins semantics are what make Cedar's guardrails trustworthy. A locked-document rule expressed as forbid will override any permit including future permits you haven't written yet. This is qualitatively different from expressing the same constraint as a negative condition inside every permit, which silently stops applying when a new permit forgets to include it.

4. Create one authorisation adapter

Application code should call a small, well-tested boundary with a request such as:

authorize({ principal, action, resource, context }) → allow/deny + decision metadata

The adapter:

  • resolves canonical IDs (opaque, immutable — not display names or email addresses);
  • loads the minimum authoritative entity slice (principal, resource, and the relationships the policies might traverse);
  • calls the Cedar authorizer;
  • handles diagnostics according to the service's safety requirements;
  • emits safe audit telemetry (request/correlation ID, principal/resource type and opaque IDs, action, decision, applicable rule identifiers and version, evaluation errors, latency);
  • blocks execution on denial.

The adapter should not accept arbitrary Cedar entities or policy text from an untrusted caller. Entity data comes from trusted application sources; policy text comes from a versioned, reviewed policy store. Never from the request.

This is the same enforcement boundary the playbook describes, now with a concrete implementation: the adapter is where route additions cannot silently bypass the decision, because every protected operation passes through it.

5. Assemble the entity slice carefully

Cedar evaluates policies against the entities you provide, not against your database. The adapter must load the minimum authoritative entity slice: the principal, the resource, and the relationships the policies might traverse (group memberships, tenant containment, ownership links).

This is where the playbook's data contract guidance becomes concrete. For each entity attribute and relationship:

  • What is the canonical source?
  • What is the freshness expectation?
  • Who owns the data?

Keep principal, action, and resource facts in their respective models. Use request context only for transient, action-specific facts such as an MFA assertion, request time, or an IP/network signal not as a duplicate source for identity, role, tenant, or resource ownership. If the entity slice is wrong, the decision is wrong, regardless of how correct the policies are.

For “may Alice read Document X?”, the adapter might supply:

Fact Cedar input Authoritative source
Who is making the request? User::"alice", including role and tenant Identity and membership service
What is being accessed? Document::"x", including owner, classification, and locked state Document service/database
How are they related? Alice's group memberships; the document's folder and tenant Membership and document data
What is true only for this request? Verified MFA assertion, request time, client network Validated session/token and request infrastructure

The policies determine which of those facts are needed. A rule that permits Group::"editors" needs Alice's group membership; a rule that permits only the document owner needs its current owner; a forbid for locked documents needs the current locked state. The adapter assembles and validates this input; the client does not.

Why "carefully": three failure modes, all silent.

  1. Missing relationships. A policy says principal in Group::"editors". If you don't load Alice's group memberships, Cedar can't match the policy. It evaluates as if Alice is in no groups. The decision is Deny even though Alice is an editor. The policy is correct; the data slice is wrong.
  2. Stale data. Alice was removed from the editors group 5 minutes ago, but your entity slice was cached from 10 minutes ago. Cedar allows a request that should be denied. The freshness question above is about this.
  3. Wrong source. You put Alice's role in the request context (which the caller controls) instead of loading it from your identity store. A malicious or buggy caller passes role: admin in the context and Cedar grants admin access. The guidance above is warning against exactly this: context is for transient facts (MFA, IP, time), not for identity, role, or ownership, because those must come from trusted sources.

The "minimum" part: you don't load your entire user database into every authorisation call. You load the slice that the policies might traverse for this request. Too little → wrong decisions. Too much → latency and unnecessary data exposure. The "carefully" is about getting that boundary right.

6. Handle diagnostics as a first-class concern

Because evaluation errors are skipped rather than fatal, diagnostics are not optional telemetry, but safety signal. A policy that errors is a policy that is not applying. If that policy was a forbid guardrail, the request may be allowed by a different permit that the forbid was meant to override.

Handle diagnostics according to the service's safety requirements:

  • Alert on evaluation errors at a rate that indicates a policy or entity-slice problem, not a transient blip.
  • Treat a diagnostics-only decision (no matching permit or forbid, but errors present) as Deny unless the service has a deliberate, documented reason to do otherwise.
  • Include diagnostic information in audit records so post-incident investigation can reconstruct why a decision was made.

Cedar-specific verification

In addition to the general test strategy from the playbook, Cedar introduces its own verification surface:

Layer Verify
Schema validation Schema compiles, entity types and actions are consistent, context shapes match action definitions
Policy unit tests Each permit and forbid produces the expected decision for representative entity slices
Entity-slice mapping tests The adapter loads the correct entities and relationships for a given request. No missing relationships, no stale data
Diagnostics-handling tests Evaluation errors are surfaced, alerted, and do not silently allow access
Schema-change tests Adding, removing, or changing entity attributes or actions does not silently alter existing decisions

For every new policy, include at least one must allow, must deny, and must not leak scenario. Test actions, not UI controls, because direct API calls and background execution bypass the UI. This is the same discipline as the general playbook, now applied to Cedar's permit/forbid semantics.

Operational checklist for Cedar

Before implementation

  • One high-value boundary chosen, with existing authorisation preserved elsewhere.
  • Cedar schema reflects the actual domain: entity types, attributes, memberships, actions, and context shapes.
  • Action vocabulary uses business actions, not HTTP verbs.
  • Entity data sources identified: canonical source, freshness, and ownership for each attribute and relationship.
  • Adapter design defined: ID resolution, entity-slice loading, Cedar invocation, diagnostics handling, audit telemetry, and enforcement.

Before rollout

  • Policies validate against the schema in CI.
  • Policy changes have review, audit, and rollback paths.
  • The adapter is the sole entry point for Cedar evaluation — no bypass paths.
  • Entity-slice loading is tested for correctness and minimal scope.
  • Diagnostics are alerted on, not just logged.
  • Existing data/query/storage controls still enforce tenant and ownership boundaries.
  • Negative and regression tests prove denial causes neither disclosure nor side effect.

After rollout

  • Inspect allow/deny rates, diagnostics, latency, and unexpected denials.
  • Confirm no route or job bypasses the adapter.
  • Review forbid guardrails, stale entity data, and policy-store changes on a defined cadence.
  • Record incidents and confusing access requests as new regression cases or schema improvements.

Cedar-specific anti-patterns

In addition to the general anti-patterns from the playbook:

  • Trusting unvalidated entity data: passing request-supplied attributes or relationships into Cedar without resolving them from a trusted source. Cedar evaluates what you give it — incorrect entity data can produce incorrect authorisation decisions.
  • Forgetting forbid when adding new permits: a new permit that broadens access may override a constraint that was previously enforced by the absence of a permit, not by an explicit forbid. Express guardrails as forbid so they survive new permits.
  • Ignoring diagnostics: treating evaluation errors as harmless noise. A skipped forbid is a silent security gap.
  • Overloading context with entity facts: using the context map to pass principal roles, tenant membership, or resource ownership instead of modelling them as entities and relationships. Context is for transient, action-specific facts.
  • Policy-store drift: changing policies without schema validation, version pinning, or regression tests. A policy that validated against the previous schema may error against the new one.
  • Entity-slice gaps: loading the principal and resource but forgetting the relationships the policies traverse (group memberships, tenant containment). A policy that depends on principal in Role::"admin" will deny if the Role entity is not in the slice — silently and without a clear error.

References


Descubra mais sobre Rafael Bernard Araujo

Assine para receber nossas notícias mais recentes por e-mail.

Deixe uma resposta

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.