Policy as Code: OPA/Rego, Cedar & Casbin

How modern authorization engines write, test, and ship the policy XACML left abstract. A tour of Open Policy Agent and Rego, AWS Cedar, and Casbin: their languages, decision models, deployment patterns, ecosystems (Gatekeeper, Conftest, Verified Permissions), and how to test and choose between them, with the same worked example implemented in all three.

The Friday change

On a Friday afternoon, someone on the platform team opens the admin screen of the authorization service and edits a permission rule to give a new contractor access to the support console. At 16:40 every support agent in the company loses access to customer accounts. It takes three hours to work out what happened. There’s no diff showing what the rule looked like before, no review that might have caught the mistake, no test that would have failed, and no way to roll back except by retyping the old rule from memory. The audit log records that a rule changed; it doesn’t record what it changed from.


The XACML article ended with a promise: you now have the architecture, PEP, PDP, PIP, PAP, the request and decision flow, combining algorithms, but not the language. Real teams don’t write raw XACML XML any more, and they shouldn’t be editing rules in an admin form on a Friday either. They write policy as code: authorization rules in a real language, checked into a real repository, tested in a real pipeline, and deployed like any other artifact. This article fills that gap with three of the languages that run production authorization today, Open Policy Agent’s Rego, AWS Cedar, and Casbin, and the tools that grew up around them.


What policy as code means

“Policy as code” isn’t simply “the policy happens to be a text file.” It’s a specific set of practices borrowed from software engineering and applied to the PAP and PRP roles of the XACML architecture, the writing and storing of rules. Policies live in Git or an equivalent rather than in a database row edited through a screen, so every change has an author, a timestamp, and a diff. Changes go through pull requests, and a second engineer, often from security or platform, reviews the diff before it merges, just as with application code. Policies have unit tests asserting that specific inputs produce specific decisions, and those tests run in CI on every change, not only when someone remembers to press “test.” Policies are packaged as a versioned artifact, a bundle, a compiled binary, or a container layer, so a decision can always be traced to the exact policy version that produced it. And promoting a policy from staging to production follows the same CI/CD path as an application deployment, with the same approvals, the same rollback, and the same audit trail. Every one of those would have saved the Friday.


The Policy-as-Code Pipeline From a local rule to a live, traceable PDP decision The Policy-as-Code Pipeline From a local rule to a live, traceable PDP decision line types pipeline step traceability link Policy Author (writes + tests) drafts a rule and tests it locally Pull Request (peer review) a second engineer reviews the diff CI Pipeline (build + test) runs the tests, builds a bundle Registry (distribution) the bundle is published & versioned v2 PDP (decision point) polls the registry and hot-loads it opens a PR merges publishes polls & pulls traces back to bundle v2.3.1 Author writes & tests · PR review · CI builds a bundle · Registry distributes · PDP hot-loads — decisions trace back
The policy-as-code lifecycle as a pipeline: an author drafts and tests a rule locally, a pull request is reviewed by a colleague, CI builds and tests a versioned bundle, a registry distributes it, and the PDP polls, pulls, and loads it without a redeploy. Every live decision traces back to the exact bundle version that produced it.

Each practice maps onto something the XACML article named: this is the PAP and PRP done with the discipline of a software supply chain instead of an admin screen. None of it is unique to one engine; OPA, Cedar, and Casbin all support the lifecycle, with different amounts of built-in tooling.


Don’t let the new vocabulary hide what stayed the same. A PDP built on Rego, Cedar, or Casbin still receives a request, evaluates it against loaded policy, and returns permit or deny or an equivalent. What changed from raw XACML is all upstream of that moment: how the rule was written, reviewed, tested, and shipped into the PDP. Policy as code is a statement about engineering process, not about the decision architecture, which is why the PEP, PDP, PIP, and PAP model still fits every engine below.


Three engines, one shape

Each engine answers the same question, may this principal perform this action on this resource, with a different language, evaluation model, and default deployment.


OPA / RegoAWS CedarCasbin
What it isGeneral-purpose policy engine (CNCF graduated)Purpose-built authorization language and engine, open-sourced by AWSConfigurable authorization library
Language styleDeclarative query language inspired by DatalogRestricted grammar of permit and forbid statementsA model file (your own request and policy shape) plus a matcher expression
Request shapeArbitrary JSON input plus dataPrincipal, action, resource, context (PARC)Whatever you define in model.conf
Typical deploymentSidecar or daemon over HTTP, embedded library, or WebAssemblyEmbedded Rust crate, or AWS Verified Permissions (managed)Embedded library call, in process, no network hop
Standout featureOne engine for Kubernetes, CI, infrastructure, and appsFormal, machine-checkable analysis of policiesFully configurable model; reproduces RBAC, ABAC, and ACL cheaply
EcosystemGatekeeper, Conftest, opa-envoy-pluginAWS Verified Permissions, Cedar AnalysisAdapters for SQL, MongoDB, Redis, etcd; SDKs in 10+ languages
Built-in testingopa test, a dedicated unit-test runnerSchema validator plus a policy analysis toolchainOrdinary unit tests in the host language

Every column still answers to the subject, action, resource, and environment shape of the previous articles. The differences lie in language design and deployment, not in what an authorization decision is.


Open Policy Agent and Rego

Open Policy Agent (OPA) is a CNCF-graduated, general-purpose policy engine: the same engine that admits or rejects a Kubernetes deployment can, unchanged, authorize an API call or gate a CI pipeline. It gets that generality by staying deliberately neutral about what you’re deciding. OPA only knows how to evaluate Rego, its query language, against two JSON documents you give it.


Every OPA evaluation combines exactly two inputs. input is the request being decided now, the JSON describing this specific action, supplied fresh on every query; in XACML terms it’s the PEP’s request. data is everything OPA already has loaded, reference data such as role assignments, ticket records, or org structures, usually synced in advance; it’s the PIP’s material, staged ahead of time rather than fetched live. Rego rules read both and produce a decision document, which doesn’t have to be a bare boolean: Rego can return structured JSON, useful for obligations or an explanation alongside the verdict.


flowchart LR
  accTitle: How a single OPA evaluation is composed
  accDescr: Two source nodes, Input and Data, converge into a Rego policy evaluation node, which produces a decision document carrying allow or deny plus optional detail.
  Input["Input<br/>(this request, JSON)"] --> Eval["Rego policy<br/>evaluation"]
  Data["Data<br/>(preloaded reference data:<br/>roles, tickets, org data)"] --> Eval
  Eval --> Decision["Decision document<br/>(allow/deny + optional detail)"]
How one OPA evaluation is put together. The request arrives as the input document; reference data already loaded into OPA is the data document; Rego rules are evaluated against both, and OPA returns a decision document that can be a boolean or a richer structured result.

Here’s the cashback app’s support rule, a support agent may view an account only with an open ticket assigned to them, during working hours, in Rego:


package cashback.authz
 
import future.keywords.in
 
default allow := false
 
allow if {
	input.action == "view"
	input.resource.type == "account"
	input.subject.role == "support-agent"
	ticket_open_for_resource
	business_hours
}
 
ticket_open_for_resource if {
	some ticket in data.tickets
	ticket.assignee == input.subject.id
	ticket.resource_id == input.resource.id
	ticket.status == "open"
}
 
business_hours if {
	input.environment.hour >= 9
	input.environment.hour < 18
}

Read it as you read the XACML pseudo-rule. default allow := false is deny by default, the instinct the XACML article’s combining-algorithm section argued for. The allow rule is a conjunction of conditions that must all hold, since lines inside a Rego rule body are joined by an implicit AND; if any fails, evaluation falls back to the default. ticket_open_for_resource searches data.tickets, the preloaded PIP material, for a matching open ticket, just as the XACML PDP asked its PIP for facts. Nothing here is exotic once you have the XACML vocabulary; it’s the same rule in a language you can lint, format, and unit-test.


OPA supports the three deployment shapes the XACML article described in the abstract. As a sidecar or daemon, it runs as its own process next to your service and is queried over local HTTP or gRPC, the dominant cloud-native pattern and the one opa-envoy-plugin uses to add authorization to an Envoy-fronted service mesh. As an embedded library, it compiles into a Go binary, or runs as WebAssembly in other runtimes, removing the network hop at the cost of an update-and-redeploy cycle per binary. As a central service, one OPA cluster is queried by many callers over the network, the simplest to keep consistent at the cost of a network dependency on every decision.


Policies and data reach a running OPA as bundles, versioned .tar.gz archives of Rego files and JSON data fetched from a bundle server, often just an object storage bucket or an OCI registry, on a polling interval and loaded without a restart. That’s the concrete mechanism behind the pipeline diagram: publishing to a distribution point means publishing a new bundle, and loading without a redeploy is OPA noticing the bundle’s revision changed and swapping the policy atomically.


Because OPA is general-purpose, it collected an ecosystem of purpose-built wrappers. OPA Gatekeeper is a Kubernetes admission controller that uses OPA to validate or change resources as they’re created or updated, rejecting a Deployment that requests hostNetwork or a Pod missing required labels; it’s policy as code applied to the infrastructure itself. Conftest is a command-line tool that runs Rego policies against configuration files, Kubernetes manifests, Terraform plans, Dockerfiles, as a CI step, so a misconfigured resource fails the pull request before it reaches a cluster. And opa-envoy-plugin runs OPA as an external authorization filter for Envoy, so a service mesh can authorize every request without each service embedding its own logic: a PEP built once, at the mesh layer, for every service behind it.


OPA ships its own unit-test runner. Test files sit alongside the policy and use the same Rego syntax:


package cashback.authz_test
 
import data.cashback.authz.allow
 
test_agent_with_open_ticket_in_hours_is_allowed if {
	allow with input as {
		"action": "view",
		"resource": {"type": "account", "id": "4471"},
		"subject": {"id": "sara", "role": "support-agent"},
		"environment": {"hour": 14},
	} with data.tickets as [{"assignee": "sara", "resource_id": "4471", "status": "open"}]
}
 
test_agent_after_hours_is_denied if {
	not allow with input as {
		"action": "view",
		"resource": {"type": "account", "id": "4471"},
		"subject": {"id": "sara", "role": "support-agent"},
		"environment": {"hour": 21},
	} with data.tickets as [{"assignee": "sara", "resource_id": "4471", "status": "open"}]
}

opa test ./policies runs every test in the tree and fails the build on any mismatch. That’s what turns policy as code from a slogan into practice: a broken authorization rule fails CI just like a broken unit test, before it reaches production. A test asserting that support agents can view accounts with open tickets would have stopped the Friday change at the pull request.


AWS Cedar

Cedar is a policy language and evaluation engine AWS designed from scratch and open-sourced in 2023, built specifically for application authorization and, unusually among these three, with formal verification as a first-class goal rather than an afterthought.


Every Cedar decision evaluates a principal, an action, a resource, and a context, the PARC model, a direct cousin of XACML’s subject, action, resource, and environment. Cedar policies aren’t general-purpose code; they’re declarative permit or forbid statements, each naming the combinations it applies to, with an optional when or unless clause for finer conditions:


permit (
    principal in Role::"support-agent",
    action == Action::"view",
    resource in ResourceType::"Account"
)
when {
    context.ticket.status == "open" &&
    context.ticket.assignee == principal &&
    context.time.hour >= 9 && context.time.hour < 18
};

Compare it with the XACML pseudo-rule. principal in Role::"support-agent", action == Action::"view", and resource in ResourceType::"Account" together are the target, the cheap filter deciding whether the policy is relevant; the when block is the condition, the finer test against attributes; and permit is the effect. Cedar didn’t invent a new shape; it gave XACML’s shape a language you’d actually want to write.


Cedar represents principals and resources as entities, JSON records with attributes and, crucially, parent relationships that model group membership and hierarchy. A user entity can list Role::"support-agent" as a parent, which is what makes principal in Role::"support-agent" match. An optional but strongly recommended schema declares which entity types, actions, and attributes are valid, and that’s what enables Cedar’s signature capability: static analysis of policies before they ever evaluate a real request.


sequenceDiagram
  accTitle: How Cedar evaluates one authorization request
  accDescr: The application asks Cedar to authorize a principal, action, resource, and context. Cedar loads entity attributes and parent hierarchy from the entity store, evaluates the request against the permit/forbid policy set authored ahead of time by the PAP, where any matching forbid policy wins, and returns Allow or Deny to the application.
  participant App as App (PEP)
  participant Cedar as Cedar (PDP)
  participant Entities as Entities (PIP)
  participant PolicySet as PolicySet (PAP)
  PolicySet-->>Cedar: permit/forbid policies (setup, before the request)
  App->>Cedar: isAuthorized(principal, action, resource, context)
  Cedar->>Entities: load attributes + parent hierarchy
  Entities-->>Cedar: entities
  Cedar->>Cedar: evaluate permit/forbid (forbid wins)
  Cedar-->>App: Allow or Deny
A Cedar authorization request. The application supplies principal, action, resource, and context; Cedar loads the relevant entities, with their parent and group relationships, from the entity store and evaluates the permit and forbid policies against the request, entities, and an optional schema, returning Allow or Deny, with forbid always taking precedence over permit.

Cedar’s evaluation rule is simple and deliberately conservative: any matching forbid wins, however many permit policies also match. It’s the deny-overrides instinct the XACML article called the safe default, built into the language instead of left as a setting.


Formal verification is what most separates Cedar from Rego. Because Cedar’s grammar is restricted, with no arbitrary loops or recursion and a bounded set of operators, the whole policy set can be handed to an SMT solver and proven to have, or lack, properties, not just tested against examples. The open-source Cedar Analysis tools can answer questions such as whether two policies ever both apply to the same request, whether one policy is fully implied by another and therefore redundant, or whether a new policy narrows access compared with the old one or might accidentally widen it. Unit tests can only sample those questions, because a test suite checks the inputs you thought to write, while formal analysis checks all inputs at once. Rego’s generality is exactly what makes the equivalent analysis intractable for arbitrary Rego. That trade-off between expressiveness and provability is the most important thing to understand when choosing between the two.


Cedar ships as a Rust crate, cedar-policy, that applications embed directly, with bindings for other languages, so the embedded library is the usual deployment. Teams that would rather not run a PDP at all can use AWS Verified Permissions, a fully managed Cedar service: a hosted PDP you call over the network, configure with a schema and policies through the AWS console or API, and that AWS runs and scales. In the XACML article’s terms, it’s a managed central PDP that speaks Cedar.


Casbin

Casbin takes a different approach from both. Instead of shipping one fixed decision model, it ships a configurable model and asks you to describe your own. That makes it less an authorization engine in the OPA and Cedar sense and more an authorization toolkit, very good at cheaply reproducing familiar patterns, ACL, RBAC, and ABAC, inside an application, with SDKs in more than ten languages, including Go, Java, Node.js, Python, PHP, .NET, and Rust.


Every Casbin deployment starts with a small model file, model.conf, that defines four things: the shape of an incoming request, the shape of a stored policy rule, the effect, which is how several matching rules combine into one decision, Casbin’s own combining algorithm, and a matcher, a boolean expression that decides whether a request matches a policy line.


[request_definition]
r = sub, obj, act
 
[policy_definition]
p = sub_rule, obj, act
 
[policy_effect]
e = some(where (p.eft == allow))
 
[matchers]
m = eval(p.sub_rule) && r.obj == p.obj && r.act == p.act

The highlighting says .ini because Casbin model files use the same section-and-key format as a classic .ini file: brackets for sections such as [request_definition], with assignments underneath. Casbin’s reader accepts any text with that structure whatever the extension, though .conf is the recommended convention, to avoid confusion with ordinary configuration files.


Policy data lives separately, usually in a CSV file or one database row per rule:


p, r.sub.Role == "support-agent" && r.sub.HasOpenTicketFor(r.obj) && r.sub.Hour >= 9 && r.sub.Hour < 18, account, view

At request time the application calls the enforcer, enforcer.Enforce(sub, obj, act), with the current subject, object, and action. Casbin evaluates each policy line’s matcher against the request, combines the results with the policy effect, and returns a boolean. HasOpenTicketFor is an ordinary function the host application registers with Casbin; its ABAC support works by letting matcher expressions call back into real code, which is how it reaches ticket or attribute data without a PIP abstraction of its own.


flowchart LR
  accTitle: How a Casbin enforcer resolves one request
  accDescr: The app calls Enforce with subject, object, and action. The enforcer draws on a model file describing the request and policy shape plus the matcher, and on separately loaded policy data. The matcher evaluates each policy line, the policy effect combines the matching results, and the enforcer returns allow or deny to the app.
  App["App calls<br/>Enforce(sub, obj, act)"] --> Enforcer["Casbin<br/>Enforcer"]
  Model["Model (model.conf):<br/>request+policy shape, matcher"] --> Enforcer
  PolicyData["Policy data<br/>(policy.csv / DB adapter)"] --> Enforcer
  Enforcer --> Matcher["Matcher evaluates<br/>each policy line"]
  Matcher --> Effect["Policy effect<br/>combines matches"]
  Effect --> Result["allow / deny<br/>(returned to App)"]
How a Casbin enforcer resolves one request. The application calls Enforce with subject, object, and action; Casbin evaluates each line of the loaded policy against the model's matcher expression, combines the matches with the configured policy effect, and returns allow or deny to the calling code, all in process, with no network hop.

Casbin separates the model from where policy rows live through adapters, pluggable storage backends for SQL databases, MongoDB, Redis, etcd, or a plain file, all behind the same interface. Swapping file-based policy for a database that a separate admin tool writes to is a one-line adapter change, not a rewrite of the model or the application’s enforcement calls, the same “change data sources without touching policy logic” property the XACML article credited to a well-separated PIP.


Casbin deliberately doesn’t compete with the other two on the same axes. It has no general-purpose query language, because the matcher is one boolean expression rather than a program, so Rego-style logic with helper rules is awkward to write directly in a matcher, though the callback escape hatch covers most of the gap in practice. It has no formal verification comparable to Cedar Analysis. And it isn’t a standalone server by default: the enforcer is a library object inside your process, so there’s no natural single PDP shared across many applications, as there is with an OPA cluster or a Verified Permissions endpoint, unless you deliberately run Casbin’s optional gRPC server yourself. What Casbin gives up in generality and provability it recovers in almost zero latency, with no network call and often no serialization, and in how quickly a team already comfortable with RBAC or ACLs can set up exactly that pattern with a few lines of configuration.


Other members of the family

Those three are worth knowing deeply, but the pattern is wider. Kyverno solves the same problem as OPA Gatekeeper, Kubernetes admission policy, with plain YAML instead of Rego, trading Rego’s generality for an easier start for teams already living in Kubernetes manifests; think of it as the “no new language” alternative to Gatekeeper rather than a general competitor to OPA. Styra DAS and similar commercial platforms wrap OPA in a management layer, with a policy-writing interface, distribution, decision logging, and impact analysis, for organizations that want OPA’s engine without building the platform around it. And Zanzibar-style engines and OpenFGA solve a different problem: relationship-based, graph-shaped authorization at the scale of Google Drive sharing, which the ReBAC article covered and the next article returns to.


Choosing among them

There’s no universally right choice, only a right choice for what a given PDP has to decide, the same lesson the XACML article closed on. As a starting point rather than a rulebook:


If you need to…Reach for…Because…
Authorize the same way across Kubernetes, CI, infrastructure, and appsOPA / RegoOne general-purpose engine and language across very different domains, with mature tooling per domain (Gatekeeper, Conftest)
Prove that policies never overlap or accidentally widen accessCedarFormal analysis is possible only because the grammar is restricted, and no other engine here offers it
Ship application authorization that AWS runs for youCedar via AWS Verified PermissionsA managed central PDP with no infrastructure to operate
Add RBAC or ABAC to one service with almost no latency and no new infrastructureCasbinAn in-process library call, no sidecar or network hop, a model shaped to your exact request
Reject a misconfigured Terraform plan or Kubernetes manifest in CIConftest (OPA / Rego)Built for policy checks against configuration, not only live requests
Model access as a graph of relationships (sharing, nesting, delegation)None of these; see ReBACA different problem shape that Zanzibar-family systems fit directly

Real systems often use more than one row at once: OPA Gatekeeper guarding the cluster, Cedar or Verified Permissions guarding the application’s permissions, and Casbin embedded in an internal admin tool that never needed a network hop. That plurality isn’t indecision; it’s choosing the tool per decision point, now with three concrete options.


One rule, three languages

Hold the cashback rule constant and watch it in all three engines: a support agent may view a customer account only if they hold an open ticket assigned to them for that account, and only during working hours, 9:00 to 18:00.


In Rego, it’s a default allow := false plus a conjunction of conditions, with a helper rule, ticket_open_for_resource, searching preloaded ticket data. It’s the most code of the three and the most explicit about how the ticket lookup happens. In Cedar, it’s a single permit with a when clause referring to context.ticket fields the caller supplied, shorter than Rego because Cedar assumes the caller has already assembled the relevant context rather than searching a bulk data document itself. In Casbin, it’s one matcher expression plus one policy row, handing the ticket check to a host-language callback, HasOpenTicketFor, instead of expressing the lookup in the policy language at all; the shortest policy text of the three, because Casbin pushes more of the logic back into ordinary application code.


Those differences are each language’s philosophy made visible. Rego wants the lookup in the policy, where it can be tested and reviewed alongside the rule. Cedar wants the lookup done before the policy runs, keeping the policy itself provable. Casbin wants the lookup handed out to code you already trust, keeping the policy layer thin. None is wrong; they optimize for different things.


Testing: making “as code” true

A policy language without a testing story isn’t really policy as code; it’s policy in a different file format. OPA has the most complete built-in story: opa test runs unit tests written in Rego itself, reports coverage with opa test --coverage, and drops into CI as one command with a pass or fail exit code, like any other language’s test runner. Cedar tests in two layers. Ordinary unit tests, asserting that isAuthorized returns the expected decision for a table of inputs, cover specific scenarios as Rego tests do, while the schema validator and Cedar Analysis add something unit tests structurally can’t: guarantees across all inputs, not just the ones someone thought to write. A mature Cedar pipeline runs both. Casbin has no policy-specific test runner, because the enforcer is an ordinary function; you test it like anything else, calling Enforce() with fixture requests in your existing framework, go test, pytest, or jest. That’s less ceremony, but it also means no equivalent of opa test’s coverage tooling or Cedar’s formal guarantees, so the rigor depends entirely on how disciplined the application’s own test suite already is.


The promise collapses if the tests exist but nobody runs them automatically. The failure mode is mundane: a developer changes a Rego rule, runs it once by hand against the case they had in mind, and merges, and a different combination of conditions, untested and unreviewed, ships broken to production. Wiring opa test, Cedar’s validator, or your language’s test suite into the CI pipeline that blocks a broken build isn’t optional polish; it’s what makes “policy as code” true. It’s also, in the end, the answer to the Friday change.


Recap

Policy as code answers the part the XACML architecture left abstract: how the rulebook is written, tested, and shipped. It’s a process rather than a language, version control, code review, automated tests, versioned artifacts, and pipeline deployment applied to the PAP and PRP roles, with the PDP still deciding the same way underneath. OPA and Rego are general-purpose: one engine and one language, input plus data producing a decision document, reused for Kubernetes admission with Gatekeeper, configuration checks in CI with Conftest, service meshes with opa-envoy-plugin, and application authorization, tested with opa test. Cedar trades generality for provability: a restricted permit and forbid grammar over a PARC request lets policies be formally analyzed in ways Rego’s flexibility makes intractable, and it ships as an embedded crate or as the managed AWS Verified Permissions. Casbin trades a fixed model for almost no latency: a configurable model.conf plus a matcher, embedded as an in-process library call, excellent for reproducing RBAC, ABAC, and ACLs inside one service and independent of storage through adapters. And real systems mix engines by decision point rather than by company-wide mandate.



Three questions to test yourself

  1. In your own words, explain why Cedar can offer formal verification of policies while Rego generally can’t. Which design choice makes the difference, and what does each language give up or gain because of it?
  2. A team wants to add authorization to a single internal admin tool with no tolerance for extra latency and no interest in running a separate service. Which of the three engines fits best, and which two structural properties of that engine, beyond “it’s simpler,” make it right?
  3. Using the cashback support rule, explain why the ticket lookup sits inside the policy in Rego but outside it, in the context or a callback, in Cedar and Casbin. What does each choice cost, and what does it buy?

Hands-on exercises

  1. Write and test a Rego policy. Install OPA, write the cashback support rule or a simplified version as Rego, and write at least three opa test cases: one that should allow, one denied for the wrong role, and one denied for being outside working hours. Run opa test and confirm they pass, then break one condition on purpose and confirm the right test catches it.
  2. Model one rule in all three languages. Take an access rule from your own work, or invent one with at least two conditions, and write it in Rego, in Cedar, and as a Casbin model.conf plus policy line. Which was easiest to write, which would be easiest for an unfamiliar reviewer to check, and which would you trust most to catch a mistake automatically?
  3. Design a CI gate. Sketch in a README, not necessarily working code, what a pull request check should look like for a Rego policy repository: what runs, in what order, and what blocks the merge. Include at least a test step and a step that would catch an accidental default allow := true. Compare your design with how OPA’s documentation integrates opa test into CI.

Frequently asked questions

What does 'policy as code' actually mean?

It means treating authorization rules the way you treat application code: written in a real language, kept in version control, reviewed in pull requests, unit-tested, and deployed through the same CI/CD pipeline as everything else, instead of living as rows in an admin screen's database or as XML nobody diffs. The rules still play the PAP and PDP roles from the XACML architecture; policy as code changes how those rules are written, tested, and shipped.

Is OPA/Rego a replacement for XACML?

It replaces XACML's XML language, not its architecture. OPA is a general-purpose policy decision point: you send it a request as JSON, it evaluates Rego rules against that input plus any loaded data, and it returns a decision document. The PEP, PDP, PIP, and PAP shape from the XACML article is fully intact underneath; OPA is a modern, developer-friendly way to build the PDP and PAP.

What is the real difference between OPA/Rego and AWS Cedar?

Both are general-purpose authorization engines with a request shaped as principal, action, resource, and context, but they optimize for different things. Rego is a flexible query language that can express almost any policy logic and even shape arbitrary decision output, at some cost to analyzability. Cedar deliberately restricts its grammar to permit and forbid statements so that policies can be formally analyzed, proven not to overlap, checked for unreachable rules, and verified against a schema, at the cost of being less expressive than Rego for unusual logic. Choose Rego for maximum flexibility across many domains (Kubernetes, CI, infrastructure); choose Cedar when you specifically want machine-checkable policies for an application's principal, action, and resource model.

Why is Casbin different from OPA and Cedar?

OPA and Cedar ship a fixed decision model reached across a request and response boundary, whether a sidecar, a service, or a library called with one input. Casbin instead ships a configurable model: you write a small model.conf describing your own request shape, policy shape, and matching expression, and it embeds directly in your application as a library call, usually with no network hop at all. That makes Casbin very good at reproducing RBAC, ACL, and simple ABAC patterns cheaply inside one service, but it isn't a standalone policy server by default and has nothing equivalent to Rego's general-purpose logic or Cedar's formal verification.

Can these policy languages be tested automatically?

Yes, and that's the whole point of calling it policy as code. OPA ships opa test, a built-in unit-test runner for Rego that runs in CI on every change. Cedar ships a validator that checks policies against a declared schema, plus an analysis toolchain (Cedar Analysis) that can prove properties like 'these two policies never both apply.' Casbin's enforcer is an ordinary function call, so it's tested with whatever framework the host language already uses, such as Go's testing package or Python's pytest. In every case the goal is the one XACML never answered well: catching a broken authorization rule in a pull request instead of in production.

Do I have to pick just one of these for my whole system?

No, and most organizations don't. It's common to see OPA Gatekeeper enforcing policy at Kubernetes admission, Cedar or a hosted equivalent guarding an application's fine-grained permissions, and Casbin embedded in an internal service that just needs simple RBAC with no network dependency. The XACML lesson applies one level up: choose the tool per decision point, based on what that PDP actually has to decide, rather than assuming one engine must own every authorization decision in the company.