Policy as Code: OPA/Rego, Cedar & Casbin
How modern authorization engines write, test, and ship the policy that XACML left abstract. A deep, diagram-heavy 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.
From architecture to language
The XACML article ended on a promise: you now have the architecture — PEP, PDP, PIP, PAP, the request/decision flow, combining algorithms — but not the language. Real teams do not write raw XACML XML anymore. They write policy as code: authorization rules expressed in a real language, checked into a real repository, tested in a real pipeline, and deployed like any other artifact. This article is that missing piece. We take the same four-role architecture and fill in three of the languages that actually run production authorization today — Open Policy Agent’s Rego, AWS Cedar, and Casbin — plus the ecosystem of tools that grew up around them.
What “policy as code” actually means
Before comparing engines, it is worth being precise about the paradigm itself, because “policy as code” is not simply “the policy happens to be a text file.” It is a specific set of practices borrowed wholesale from software engineering and applied to the PAP and PRP roles from the XACML architecture — the authoring and storage of rules:
- Version-controlled. Policies live in Git (or an equivalent), not in a database row edited through an admin screen. Every change has an author, a timestamp, and a diff.
- Reviewed like code. Changes go through pull requests. A second engineer — often from a security or platform team — reviews the diff before it merges, the same as an application code change.
- Tested automatically. Policies have unit tests that assert specific inputs produce specific decisions, and those tests run in CI on every change, not just when someone remembers to click “test” in an admin UI.
- Built and versioned as an artifact. Policies are packaged (a bundle, a compiled binary, a container layer) with a version identifier, the same way application code is packaged, so a decision can always be traced back to the exact policy version that produced it.
- Deployed through a pipeline, not a form. Promoting a policy from staging to production follows the same CI/CD path as an application deploy — with the same approvals, the same rollback mechanism, and the same audit trail.
Every practice on that list maps onto something the XACML article already named: this is just the PAP and PRP done with the discipline of a software supply chain instead of an admin GUI. Nothing here is unique to any one engine — OPA, Cedar, and Casbin all support this lifecycle, though (as you will see) they support it with different amounts of built-in tooling.
Three engines, one shape — a first look
Before the deep dives, here is the orientation table. Each engine answers the same underlying question — “does this principal may do this action on this resource?” — with a different language, evaluation model, and default deployment shape.
| OPA / Rego | AWS Cedar | Casbin | |
|---|---|---|---|
| What it is | General-purpose policy engine (CNCF graduated) | Purpose-built authorization language and engine, open-sourced by AWS | Configurable authorization library |
| Language style | Declarative, Datalog-inspired query language | Restricted grammar: permit/forbid statements | Model file (your own request/policy shape) + a matcher expression |
| Request shape | Arbitrary JSON input + data | Principal, Action, Resource, Context (PARC) | Whatever you define in model.conf |
| Typical deployment | Sidecar/daemon over HTTP, or embedded library, or WASM | Embedded Rust crate, or AWS Verified Permissions (managed) | Embedded library call, in-process, no network hop |
| Standout feature | General-purpose: same engine for Kubernetes, CI, infra, apps | Formal, machine-checkable analysis of policies | Model is fully configurable — reproduces RBAC/ABAC/ACL cheaply |
| Ecosystem | Gatekeeper, Conftest, opa-envoy-plugin | AWS Verified Permissions, Cedar Analysis | Adapters for SQL/Mongo/Redis/etcd, SDKs in 10+ languages |
| Built-in testing | opa test — first-class unit-test runner | Schema validator + policy analysis toolchain | Ordinary unit tests in the host language |
Notice that every column still answers to the PARC/subject-action-resource-environment shape the previous two articles established — the differences are in language design and deployment, not in what an authorization decision fundamentally consists of. With the map in hand, let’s walk each engine in turn.
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, unmodified, authorize an API call or gate a CI pipeline. It achieves that generality by staying deliberately unopinionated about what you are deciding — OPA only knows how to evaluate Rego, its purpose-built query language, against two JSON documents you give it.
How OPA evaluates a decision
Every OPA evaluation combines exactly two inputs. input is the request being decided right now — the JSON document describing this specific action, supplied fresh on every query (the PEP’s request, in XACML terms). data is everything OPA already has loaded — reference data such as role assignments, ticket records, or organizational hierarchies, typically synced ahead of time (the PIP’s material, pre-staged rather than fetched live). Rego rules read both and produce a decision document — not necessarily a bare boolean; Rego can return structured JSON, which is useful for returning obligations or a full 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)"]
A minimal Rego policy for the cashback app’s support-agent rule — a support agent may view an account only with an open, assigned ticket, during business hours — reads like this:
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 the way you read the XACML pseudo-rule earlier: default allow := false is the deny-by-default posture — the same instinct the previous article’s combining-algorithm section argued for. The allow rule is a conjunction of conditions that must all hold (Rego’s implicit AND between lines inside a rule body); if any one fails, evaluation falls through to the default. ticket_open_for_resource searches data.tickets — the preloaded PIP material — for a matching open ticket, exactly the way the XACML PDP asked its PIP for facts. Nothing here is exotic once you have the XACML vocabulary; it is the same rule, in a language you can lint, format, and unit test.
Deployment: three shapes, one engine
OPA supports the same three deployment shapes the XACML article introduced in the abstract, and this is where they become concrete. As a sidecar or daemon, OPA runs as its own process next to your service and is queried over local HTTP or gRPC — the dominant cloud-native pattern, and the shape used by opa-envoy-plugin to add authorization to an Envoy-fronted service mesh. As an embedded library, OPA compiles directly into a Go binary (or runs as WebAssembly inside other runtimes), eliminating the network hop entirely at the cost of an update-and-redeploy cycle per binary. As a centralized service, one OPA cluster is queried by many callers over the network — simplest to keep consistent, at the cost of a network dependency on every decision’s critical path.
Policies and data reach a running OPA instance as bundles — versioned .tar.gz archives containing 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 hot-loaded without a restart. This is the concrete mechanism behind the pipeline diagram earlier: “publish to a distribution point” means publishing a new bundle, and “hot-load without redeploy” is OPA noticing the bundle’s revision changed and swapping policy atomically.
The ecosystem: where OPA shows up beyond one app
Because OPA is general-purpose, it accumulated an ecosystem of purpose-built wrappers rather than staying a single tool:
- OPA Gatekeeper — a Kubernetes admission controller that wraps OPA to validate or mutate resources as they are created or updated (rejecting a Deployment that requests
hostNetwork, or a Pod missing required labels). This is policy as code applied to infrastructure itself, not application requests. - Conftest — a CLI 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 ever reaches a cluster.
opa-envoy-plugin— runs OPA as an external authorization filter for Envoy, letting a service mesh authorize every request without each service embedding its own logic — a PEP built once, at the mesh layer, serving every service behind it.
Testing Rego: opa test
OPA ships a built-in unit-test runner. Test files live alongside policy files and use the same Rego syntax, asserting specific inputs produce specific results:
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 assertion mismatch — the exact mechanism that turns “policy as code” from a slogan into an enforced practice: a broken authorization rule fails CI the same way a broken unit test does, before it reaches production.
AWS Cedar
Cedar is a policy language and evaluation engine that AWS designed from scratch and open-sourced in 2023, built specifically for application-level authorization and, unusually among these three, with formal verification as a first-class design goal rather than an afterthought.
The PARC request model
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/environment. Cedar policies are not general-purpose code; they are declarative permit or forbid statements, each naming which principal/action/resource combinations it applies to, with an optional when/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 this line by line with the XACML pseudo-rule from the previous article: principal in Role::"support-agent" and action == Action::"view" and resource in ResourceType::"Account" together are the target — the cheap filter deciding whether this policy is even relevant; the when block is the condition — the finer test against attributes; permit is the effect. Cedar did not invent a new shape, it gave XACML’s shape a language you would actually want to write.
Entities, schema, and the decision flow
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, which is 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
Cedar’s evaluation rule is simple and deliberately conservative: any matching forbid policy wins, regardless of how many permit policies also match — the same deny-overrides instinct the XACML article called the safe default, just built into the language rather than left as a configuration choice.
Why formal verification is Cedar’s real differentiator
This is the feature that most separates Cedar from Rego. Because Cedar’s grammar is deliberately restricted — no arbitrary loops or recursion, a bounded set of operators — the policy set as a whole can be fed to an SMT solver and proven to have (or lack) properties, not just tested against example inputs. The open-source Cedar Analysis tooling can answer questions like “do these two policies ever both apply to the same request?”, “is this policy fully implied by that one (making it redundant)?”, or “does this new policy narrow access compared to the old one, or could it accidentally widen it?” — questions that unit tests can only sample, never answer exhaustively, because a test suite checks the inputs you thought to write, while formal analysis checks all inputs at once. Rego’s generality is precisely what makes the equivalent analysis intractable for arbitrary Rego — you cannot generally prove properties about a Turing-complete-adjacent query language the way you can about Cedar’s restricted grammar. That trade-off — expressiveness versus provability — is the single most important engineering decision to understand when choosing between the two.
Deployment: embedded crate or managed service
Cedar ships as a Rust crate (cedar-policy) that applications embed directly, with bindings for other languages, making the embedded-library deployment shape the common case. For teams that would rather not run their own PDP at all, AWS Verified Permissions is a fully managed Cedar evaluation service — a hosted PDP you call over the network, configure with a schema and policies through the AWS console or API, and that AWS operates and scales. Verified Permissions is, in the XACML article’s terms, a managed centralized-service PDP that happens to speak Cedar.
Casbin
Casbin takes a structurally different approach from both OPA and Cedar: instead of shipping one fixed decision model, it ships a configurable model and asks you to describe your own. This makes Casbin less an “authorization engine” in the OPA/Cedar sense and more an authorization toolkit — extremely good at cheaply reproducing familiar patterns (ACL, RBAC, ABAC) directly inside an application, with SDKs in more than ten languages (Go, Java, Node.js, Python, PHP, .NET, Rust, and others).
The model: request, policy, matcher
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 (how multiple matching rules combine into one decision — Casbin’s own combining algorithm, in XACML’s vocabulary), and a matcher — a boolean expression that decides whether a given request matches a given 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.actPolicy data lives separately, typically in a CSV file or a database row per rule:
p, r.sub.Role == "support-agent" && r.sub.HasOpenTicketFor(r.obj) && r.sub.Hour >= 9 && r.sub.Hour < 18, account, viewAt request time, the application calls the enforcer — enforcer.Enforce(sub, obj, act) — with the current subject, object, and action; Casbin evaluates every policy line’s matcher against that request, folds the results using the policy effect, and returns a boolean. HasOpenTicketFor in the example is an ordinary function registered with Casbin from the host application — Casbin’s ABAC support works by letting matcher expressions call back into real code, which is how it reaches ticket or attribute data without needing its own PIP abstraction.
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)"]
Adapters: policy storage without leaving the model alone
Casbin separates the model from where policy rows actually live via adapters — pluggable storage backends for SQL databases, MongoDB, Redis, etcd, or a plain file, all satisfying the same interface. Swapping a file-based policy for a database-backed one 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 “swap data sources without touching policy logic” property the XACML article credited to a well-separated PIP, achieved here through the adapter abstraction instead.
Why Casbin is not a drop-in replacement for OPA or Cedar
Casbin deliberately does not compete on the same axes as the other two. It has no general-purpose query language — the matcher is one boolean expression, not a program, so Rego-style multi-step logic with helper rules is awkward to express directly in a matcher (though the callback-function escape hatch covers most of the gap in practice). It has no formal verification story comparable to Cedar Analysis. And critically, it is not a standalone server by default — the enforcer is a library object living inside your process, so there is no natural single PDP shared consistently across many applications the way an OPA cluster or Verified Permissions endpoint is, unless you deliberately stand up Casbin’s optional gRPC-based Casbin Server yourself. What Casbin trades away in generality and provability, it recovers in near-zero latency (no network call, often no serialization) and in how quickly a team already fluent in RBAC or ACL thinking can stand up exactly that pattern with a few lines of config.
Other members of the policy-as-code family
The three engines above are the ones worth knowing deeply, but the pattern is bigger than any three tools, and a professional map of this space should include the near neighbors:
- Kyverno solves the same problem as OPA Gatekeeper — Kubernetes admission policy — but with plain YAML instead of Rego, trading Rego’s generality for a lower barrier to entry for teams that live in Kubernetes manifests already. It is worth knowing as the “no new language” alternative to Gatekeeper specifically, not as a general-purpose competitor to OPA.
- Styra DAS and similar commercial platforms wrap OPA with a management plane — policy authoring UI, distribution, decision logging, impact analysis — for organizations that want OPA’s engine without building the surrounding platform themselves.
- Zanzibar-style engines and OpenFGA solve a different problem this article deliberately did not cover: relationship-based, graph-shaped authorization at Google-Drive-sharing scale. They deserve — and get — their own article next, because they answer “how do you check access when the answer depends on a graph of relationships too large to evaluate as a flat rule,” which is a different question from “what language do I write my rules in.”
Choosing among them
There is no universally correct choice — only a correct choice for what a given PDP actually needs to decide, the same lesson the XACML article closed on. Use this as a starting heuristic, not a rulebook:
| If you need to… | Reach for… | Because… |
|---|---|---|
| Authorize the same way across Kubernetes, CI, infra, and apps | OPA / Rego | One general-purpose engine and language across very different domains, with mature ecosystem tooling per domain (Gatekeeper, Conftest) |
| Prove that policies never overlap or accidentally widen access | Cedar | Formal analysis is only possible because the grammar is deliberately restricted — no other engine here offers it |
| Ship application-level authorization AWS will operate for you | Cedar via AWS Verified Permissions | A managed centralized PDP, no infrastructure to run |
| Add RBAC/ABAC to one service with near-zero latency and no new infra | Casbin | In-process library call, no sidecar or network hop, model tailored to exactly your request shape |
| Reject a misconfigured Terraform plan or Kubernetes manifest in CI | Conftest (OPA/Rego) | Purpose-built for policy-as-code checks against configuration, not just live requests |
| Model access as a graph of relationships (sharing, nesting, delegation) | Neither — see the next article | This is a different problem shape; Zanzibar-family systems fit it directly |
Real systems frequently use more than one row of this table at once — OPA/Gatekeeper guarding the cluster, Cedar or Verified Permissions guarding the application’s resource permissions, Casbin embedded in an internal admin tool that never needed a network hop in the first place. That plurality is not indecision; it is the same “pick the tool per PDP” principle the previous article’s deployment section introduced, now with three concrete options to pick from.
Worked example: one rule, three languages
Return to the cashback app’s support-agent rule one last time, and watch the same policy expressed in all three engines. Holding a single fixed rule constant while the language changes is the fastest way to see what each language actually costs and buys you.
The rule, in prose: a support agent may view a customer account only if they hold an open ticket assigned to them for that account, and only during business hours (9:00–18:00).
In Rego (shown in full above): a default allow := false plus a conjunction of conditions, with a helper rule (ticket_open_for_resource) searching preloaded ticket data — the most code of the three, and the most explicit about how the ticket lookup happens.
In Cedar (shown in full above): a single permit statement with a when clause referencing context.ticket fields the caller supplied — shorter than Rego because Cedar assumes the caller assembled the relevant context already, rather than searching a bulk data document itself.
In Casbin, the same rule becomes one matcher expression plus one policy row, delegating the ticket check to a host-language callback function (HasOpenTicketFor) rather than 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.
The differences are not accidental — they are each language’s philosophy made visible in one example. Rego wants the lookup logic in the policy, testable and reviewable alongside the rule. Cedar wants the lookup done before the policy runs, keeping the policy itself provable. Casbin wants the lookup delegated out to code you already trust, keeping the policy layer thin. None of the three is wrong; they are optimizing for different things, and now you can see exactly what.
Testing policy as code: making “as code” true in practice
A policy language without a test story is not really policy as code — it is just policy in a different file format. Each engine takes this seriously, with different tooling maturity:
OPA has the most complete built-in story: opa test runs table-style unit tests written in Rego itself, supports coverage reporting (opa test --coverage), and integrates directly into CI as a single command with a pass/fail exit code — the same shape as any other language’s test runner.
Cedar tests in two layers. Ordinary unit tests (assert isAuthorized(request) returns the expected decision for a table of inputs) cover specific scenarios the way Rego tests do, while the schema validator and Cedar Analysis toolchain add something unit tests structurally cannot: exhaustive guarantees across all inputs, not just the ones a test author thought to write. A mature Cedar pipeline runs both — unit tests for behavior, analysis for structural guarantees.
Casbin has no policy-specific test runner because the enforcer is an ordinary function in the host language — you test it the way you test anything else, calling Enforce() with fixture requests inside your existing test framework (go test, pytest, jest). This is lower ceremony than the other two, but it also means Casbin has no equivalent to opa test’s dedicated coverage tooling or Cedar’s formal guarantees; the rigor is entirely a function of how disciplined the surrounding application’s test suite already is.
Recap
Policy as code is the answer to the piece the XACML architecture left abstract — how the rulebook actually gets written, tested, and shipped:
- Policy as code is a process, not a language. Version control, code review, automated tests, versioned artifacts, and pipeline deployment — applied to the PAP/PRP roles from the XACML architecture. The PDP still decides the same way underneath.
- OPA/Rego is general-purpose. One engine, one language (
input+data→ decision document), reused across Kubernetes admission (Gatekeeper), CI config checks (Conftest), service meshes (opa-envoy-plugin), and application authorization alike, tested with the built-inopa test. - Cedar trades generality for provability. A restricted
permit/forbidgrammar over a PARC request lets policies be formally analyzed — proven non-overlapping, checked against a schema — which Rego’s flexibility makes intractable; ships as an embedded crate or as the managed AWS Verified Permissions service. - Casbin trades a fixed model for near-zero latency. A configurable
model.confplus a matcher expression, embedded as an in-process library call with no network hop, excellent for reproducing RBAC/ABAC/ACL cheaply inside one service, storage-agnostic via adapters. - Real systems mix engines by PDP, not by company-wide mandate — the same “pick per decision point” principle the XACML article closed with, now with three concrete, production-proven options.
Three questions to test yourself
- Explain, in your own words, why Cedar can offer formal verification of policies while Rego generally cannot — what specific design choice makes the difference, and what does each language give up or gain because of it?
- A team wants to add authorization to a single internal admin tool with no tolerance for added request latency and no interest in running a separate service. Which of the three engines fits best, and which two structural properties of that engine (not just “it’s simpler”) make it the right fit?
- Using the cashback support-agent rule, explain why the same policy is expressed with the ticket lookup inside the policy in Rego, but outside the policy (in context or a callback) in Cedar and Casbin. What does each choice cost, and what does it buy?
Hands-on exercises
- Write and test a Rego policy. Install OPA locally, write the cashback support-agent rule (or a simplified version) as a Rego policy, and write at least three
opa testcases: one that should allow, one denied for the wrong role, and one denied for being outside business hours. Runopa testand confirm all three pass, then break one condition on purpose and confirm the right test catches it. - Model one rule in all three languages. Pick any access rule from your own work (or invent one with at least two conditions), and write it in Rego, Cedar, and a Casbin model.conf + policy line. Compare the three: which was easiest to write, which would be easiest for a reviewer unfamiliar with the tool to check, and which would you trust most to catch a mistake automatically?
- 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 minimum a test step and a step that would catch an accidental
default allow := true. Compare your design againstopa test’s actual CI integration in OPA’s documentation.
Frequently asked questions
What does 'policy as code' actually mean?
It means treating authorization rules the same way you treat application code: written in a real language, stored 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 UI's database or as unreviewable XML nobody diffs. The rules still play the PAP/PDP roles from the XACML architecture; policy as code just changes how those rules are authored, 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/PAP shape from the previous article is fully intact underneath — OPA is just 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 PARC-shaped request (principal/action/resource plus 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, which costs some analyzability. Cedar deliberately restricts its grammar to permit/forbid statements so that policies can be formally analyzed — proven non-overlapping, checked for unreachable rules, verified against a schema — at the cost of being less expressive than Rego for exotic logic. Choose Rego for maximum flexibility across many domains (Kubernetes, CI, infra); choose Cedar when you specifically want machine-checkable authorization policies for an application's principal/action/resource model.
Why is Casbin different from OPA and Cedar?
OPA and Cedar ship a fixed decision model reached over a request/response boundary (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 embeds directly into your application as a library call, most often without a network hop at all. That makes Casbin exceptionally good at reproducing RBAC, ACL, and simple ABAC patterns cheaply inside a single service, but it is not a standalone policy server by default and it has no equivalent to Rego's general-purpose logic or Cedar's formal verification.
Can these policy languages be tested automatically?
Yes, and doing so is the entire point of calling this 'policy as code.' OPA ships `opa test`, a built-in unit-test runner for Rego rules 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 a plain function call, so it is unit tested with whatever test framework the host language already uses — Go's `testing`, Python's `pytest`, and so on. In every case, the goal is the same one XACML never had a good answer for: 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 in practice most organizations do not. It is common to see OPA/Gatekeeper enforcing policy at the Kubernetes admission layer, Cedar or a hosted equivalent guarding an application's fine-grained resource permissions, and Casbin embedded inside an internal service that just needs simple RBAC with no network dependency. The XACML lesson from the previous article still applies here at one level up: pick the tool per PDP, based on what that PDP actually needs to decide, rather than assuming one engine must own every authorization decision in the company.