Skip to main content

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_tiers is 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 -v to see each test name and its result. The summary line reports PASS: 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):

  1. Add a "svc-export": "write" entry to account_tiers in policy.rego. Confirm write_accounts picks it up with no other change; that is the comprehension doing its job.
  2. Add one allow test proving svc-export may POST, and one deny test proving svc-audit (a read-tier account) may not PUT.
  3. Now change default allow := false to default 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

  1. 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?
  2. 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.
  3. What does with input as {...} do inside a test, and why is it necessary?