Open Policy Agent (OPA)

How Neon Law Navigator runs authorization as a separate decision engine. OPA is the decision point; web is the enforcement point. The split lets policy change without redeploying the binary, and keeps one decision engine that other services can adopt later.

This page owns the system — deployment shape, the query mechanics, Rego authoring, and the Rust client. The semantics of who-can-see-what (the input document, the allow rules, admin bypass, project scoping) live in access-model.md; read that first for any change to what a rule decides.

Deployment shape

OPA runs as a sidecar in the navigator-web Pod:

The manifest is k8s/base/opa/opa.yaml — the OPA Deployment plus the ConfigMap that carries the policy. In production the sidecar is what enforces decisions; a standalone Deployment is a debugging convenience that lets you kubectl exec in and run opa eval against the live policy.

Query API

The web server posts request metadata to <base>/v1/data/navigator/authz/allow and reads back result.allow (a boolean). <base> comes from NAVIGATOR_OPA_URL.

POST http://localhost:8181/v1/data/navigator/authz/allow
Content-Type: application/json

The request body is the input document — its exact fields (path as a segment array, method, session.role, project_id) are canonical in access-model.md. /v1/data/<package>/<rule> is the query shape; the package and rule names must match the Rego.

Rego policy

The live policy ships in k8s/base/opa/opa.yaml under package navigator.authz, with tests in k8s/base/opa/navigator_test.rego. Rules to keep:

Local Rego development

# Evaluate a policy + input pair without OPA running:
opa eval -d k8s/base/opa/ -i input.json 'data.navigator.authz.allow'

# Run the test suite:
opa test -v k8s/base/opa/

# Format:
opa fmt -w k8s/base/opa/

The opa CLI is the single tool; install it via Homebrew, scoop, or the official release. Don't shell out to it from Rust — call the REST API.

Updating policy

# Edit the ConfigMap, then apply:
kubectl --context kind-navigator -n navigator apply -f k8s/base/opa/opa.yaml

# OPA hot-reloads the ConfigMap mount (the container runs with --watch); no restart needed. Verify:
kubectl --context kind-navigator -n navigator dev logs -l app=opa --tail=20

Decision logs

OPA can emit a structured decision log per query (input, result, policy version). With decision_logs.console = true in OPA's config, each decision prints as JSON to stdout and is pickable by kubectl logs. Wire it to the OTel collector or stdout in dev — it is what answers "did OPA say yes or no for that user at 14:32" after an incident.

The Rust client

web::policy (web/src/policy.rs) holds both halves:

Anti-patterns