Bundles and testing with opa test
Objective
Explain what an OPA bundle is and how a PDP loads it, and write an opa test suite with both allow and deny cases that verify a policy behaves as intended.
Concept
Bundles
A running PDP does not read your policy files off a laptop. It loads a bundle: a gzipped tarball containing the compiled policy (.rego) and any data (data.json or data.yaml) the policy references under data. OPA fetches bundles over HTTP from a bundle server, verifies them, and activates them atomically, so the PDP is always evaluating a complete, consistent snapshot. If a new bundle fails to verify or parse, OPA keeps serving the last good one.
Two ideas matter for how you structure a policy:
- Policy and data ship together. In the lab,
account_tiersis written inline so the lab is self-contained. In production that allow list is data in the bundle, so the same policy serves different environments by shipping different data. Nothing in the policy logic changes. - Bundles are versioned and signed. A bundle carries a revision, and OPA can require bundle signatures so a PDP only activates content from a trusted source. This is the delivery mechanism a control plane like Writ builds on when it promotes signed bundles from Git through dev, staging, and prod.
A bundle is standard OPA. You can build one with the opa build command and serve it yourself; a control plane just automates building, signing, promoting, and logging.
Testing with opa test
Policy is code, so it needs tests. opa test discovers rules named test_* and runs each as an independent evaluation. A test passes when its body holds. You assert allow and deny with the same tool:
package university.opa_eopa.fundamentals_test
import data.university.opa_eopa.fundamentals
# Allow: assert the rule is true for a valid request.
test_read_account_can_get if {
fundamentals.allow with input as {"subject": {"account": "svc-reporting"}, "method": "GET"}
}
# Deny: assert the rule is NOT true for a request that should be rejected.
test_read_account_cannot_post if {
not fundamentals.allow with input as {"subject": {"account": "svc-reporting"}, "method": "POST"}
}
Three details:
with input as {...}substitutes a synthetic input document for that one evaluation, so each test controls exactly the request it is checking.- The deny tests use
not. They are the ones that catch a false allow, a request granted when policy says it should be denied. A suite with only allow tests can pass while the policy is dangerously permissive. - Run with
-vto see each test name and its result. The summary line reportsPASS: N/N.
git clone https://github.com/EnforceAuth/university-labs.git
cd university-labs
opa test courses/opa-eopa-fundamentals -v
From the cloned university-labs repo, or cd into courses/<course-id>/ and pass . as the path.
Hands-on lab
Open ../lab/policy_test.rego. It has three allow tests and three deny tests. Run the suite and confirm PASS: 6/6. Then extend it (the same exercise the lab README walks through):
- Add a
"svc-export": "write"entry toaccount_tiersinpolicy.rego. Confirmwrite_accountspicks it up with no other change; that is the comprehension doing its job. - Add one allow test proving
svc-exportmayPOST, and one deny test provingsvc-audit(a read-tier account) may notPUT. - Now change
default allow := falsetodefault allow := true. Rerun the suite. Note which tests fail and set it back. The failures you see are exactly the deny cases, which is why deny tests exist.
Check for understanding
- What two kinds of content does an OPA bundle contain, and why is it useful that the same policy can be shipped with different data per environment?
- Why does a test suite need deny tests and not just allow tests? Give the term for the bug a missing deny test would hide.
- What does
with input as {...}do inside a test, and why is it necessary?