Skip to main content

Serving AuthZEN from OPA with opa-authzen-plugin

Objective

Explain how opa-authzen-plugin turns OPA into an AuthZEN-conformant PDP, map an AuthZEN request onto Rego input, and point the plugin at the decision rule the lab policy defines.

Concept

OPA already evaluates policy and returns decisions. What it does not do out of the box is speak AuthZEN on the wire. opa-authzen-plugin closes that gap: it extends OPA with the AuthZEN Authorization API 1.0 so a standards-based PEP can call OPA without knowing OPA's native REST shape.

The plugin adds three surfaces to an OPA server:

  • POST /access/v1/evaluation — one AuthZEN request in, one {"decision": ...} out.
  • POST /access/v1/evaluations — a batch of requests in, an ordered list of decisions out.
  • GET /.well-known/authzen-configuration — the discovery document a PEP reads to find those endpoints.

From wire to input

The plugin's job on each call is a translation. It takes the AuthZEN request body and presents it to your policy as input, preserving the object names:

AuthZEN request body Rego input
-------------------- ----------
subject { ... } --> input.subject
action { "name": } --> input.action.name
resource { ... } --> input.resource
context { ... } --> input.context

So the policy you author is ordinary Rego over input. That is exactly what the lab does. The plugin then reads a single decision value out of your policy and returns it as the AuthZEN decision. You point the plugin at the decision rule to evaluate (in the lab, data.university.authzen.evaluation.decision); the plugin does not invent policy, it routes to yours.

flowchart LR
PEP[PEP] -->|POST /access/v1/evaluation| P[opa-authzen-plugin on OPA]
P -->|maps body to input| OPA[OPA policy: decision]
OPA -->|bool| P
P -->|decision: bool| PEP

Text version of the diagram: the PEP sends an AuthZEN evaluation to the plugin running on OPA; the plugin maps the request body onto input and evaluates the configured decision rule; OPA returns a boolean; the plugin returns it to the PEP as the AuthZEN decision.

Serving it concretely

The plugin is compiled into an OPA build and enabled through OPA's configuration, which also binds it to the decision rule to route to (for this lab, data.university.authzen.evaluation.decision). You then start OPA in server mode with stock OPA:

opa run --server

--server (short -s) is ordinary OPA; the plugin adds the AuthZEN routes on top of the running server. A PEP posts an AuthZEN request to the single-evaluation endpoint the plugin registered:

curl -s localhost:8181/access/v1/evaluation \
-H 'content-type: application/json' \
-d '{
"subject": {"type": "user", "id": "alice@acme.com", "properties": {"roles": ["editor"], "department": "eng"}},
"action": {"name": "can_read"},
"resource": {"type": "document", "id": "doc-1", "properties": {"owner": "alice@acme.com", "department": "eng", "classification": "internal"}},
"context": {}
}'
# => {"decision": true}

Port 8181 is OPA's default server port. The exact config keys that enable the plugin and bind the decision path live in the plugin's build and config, so they are omitted here; the route, request shape, and response shown are the AuthZEN 1.0 contract.

Why this matters

Because the interface is AuthZEN and not an OPA-specific format, the PEP is decoupled from the PDP. You can move from open-source OPA to Enterprise OPA, or run several PDPs, without touching the PEP. Under EnforceAuth, Writ governs the policy bundle these PDPs load: it signs bundles from Git, promotes them dev to staging to prod, and keeps decision logs for replay. Writ is the control plane over the PDPs; it is not itself on the request path. The plugin and OPA are what answer the AuthZEN call.

What the plugin does not change

The plugin does not add a new policy language, a new runtime, or a new CLI. You still author Rego, you still run opa, and you still test with opa test. The plugin is an interface adapter that makes OPA answer in the AuthZEN dialect. There is no enforceauth run runtime and no bespoke evaluation command; the decision is plain OPA.

Hands-on lab

You will not run a live server here (the graded artifact is the policy and its tests), but you will author the decision the plugin evaluates. Open ../lab/policy.rego: it ships as a default-deny starter with a TODO(learner) where the allow rules go. First trace the fields the plugin maps onto input, because your rules read them:

  • The plugin would set input.subject from the request subject. Your rules read input.subject.id and input.subject.properties.roles/.department.
  • The plugin would set input.action. Your rules read input.action.name.
  • The plugin would set input.resource. Your rules read input.resource.properties.owner, .department, and .classification.

Now implement the TODO: OR several small allow if { ... } rules together and set decision if allow, one rule per bullet in the lab's Rules section (admin, owner, editor-in-department, public-read). Run the suite from the cloned university-labs repo and iterate until it is green:

git clone https://github.com/EnforceAuth/university-labs.git
cd university-labs
opa test courses/authzen-pdp-integration -v

The untouched starter reports PASS: 5/9 (deny cases pass, allow cases fail). Keep going until you reach PASS: 9/9. Every with input as {...} block is a body the plugin would hand your policy after mapping an AuthZEN request; the decision rule is the value the plugin reads back and returns. See ../lab/README.md for the full exercise and stretch goals.

Check for understanding

  1. Which three HTTP surfaces does opa-authzen-plugin add to an OPA server?
  2. An AuthZEN request has "action": {"name": "can_edit"}. What is the exact Rego path your policy reads to get the string can_edit?
  3. True or false: the plugin introduces a new policy language and a new runtime command. Explain.
  4. In the EnforceAuth model, which component governs the policy bundle the PDP loads, and is that component on the request path?