Skip to main content

Rego basics: rules, helpers, in, and comprehensions

Objective

Read and write the core Rego v1 constructs: a default-deny allow rule, a helper rule that other rules depend on, the in membership operator, and a comprehension that builds a set from existing data.

Concept

Rego is the declarative language OPA evaluates. A policy is a set of rules in a package. Each rule assigns a value when its body holds; if the body does not hold, the rule is undefined. This is why default-deny works: you declare default allow := false, then add rules that make allow true only in the cases you intend.

Rules

A complete rule assigns a scalar or a value:

default allow := false

allow if input.subject.role == "admin"

allow is false unless the caller's role is admin. The if keyword introduces the rule body. Multiple rules with the same name are a logical OR: allow is true if any one of them holds. Nothing outside the package can make allow true by accident, because there is no rule that does so.

Helper rules

A helper rule is just a rule other rules read. It names a sub-decision so the main rule stays readable and the sub-decision can be reused:

# Helper: the tier for the calling account, or undefined if not allow-listed.
subject_tier := account_tiers[input.subject.account]

allow if {
subject_tier in {"read", "write"}
input.method in read_methods
}

If input.subject.account is not a key in account_tiers, subject_tier is undefined, so every rule that references it fails and allow falls through to false. Undefined is not an error in Rego; it is the normal way a lookup that finds nothing propagates to a deny.

The in operator

in tests membership in a set, array, or object. It replaces the older, error-prone collection[x] iteration idiom for membership checks and reads the way you say it out loud:

input.method in {"GET", "HEAD"} # is the method one of these?
input.subject.account in write_accounts # is the account in the write set?

Prefer in for "is X a member" questions. It is explicit and does not accidentally introduce iteration.

Comprehensions

A comprehension builds a new collection by iterating existing data. A set comprehension uses { ... | ... }:

# Every account whose tier is "write".
write_accounts := {account | some account, tier in account_tiers; tier == "write"}

Read it right to left: some account, tier in account_tiers iterates the object's key/value pairs, the semicolon adds the filter tier == "write", and the head account collects the matching keys into a set. Because write_accounts is derived from account_tiers, editing the allow list automatically updates the write set. There are also array comprehensions [ ... | ... ] and object comprehensions {k: v | ...}.

Hands-on lab

Open courses/opa-eopa-fundamentals/policy.rego. The scaffolding is written for you: default allow := false, the account_tiers allow list, the read_methods and write_methods sets, the subject_tier helper rule, and the write_accounts set comprehension. The two decision rules are left as a # TODO(learner) stub, so the starter is red on purpose (PASS: 3/6, the three allow tests fail).

Your task is to implement the TODO using the four constructs from this lesson:

  1. A read-tier or write-tier account may call a read method — check subject_tier in {"read", "write"} and input.method in read_methods.
  2. Only a write-tier account may call a write method — check input.subject.account in write_accounts and input.method in write_methods.

Write them as two separate allow if rules (multiple rules with the same name are a logical OR). Run the suite until it is green:

git clone https://github.com/EnforceAuth/university-labs.git
cd university-labs
opa test courses/opa-eopa-fundamentals -v

Aim for PASS: 6/6. Keep iterating until opa test is green. Before you run anything, predict the decision for each of these inputs and confirm it once your rules pass:

  • {"subject": {"account": "svc-checkout"}, "method": "POST"}
  • {"subject": {"account": "svc-reporting"}, "method": "POST"}
  • {"subject": {"account": "svc-unknown"}, "method": "GET"}

Check for understanding

  1. In the policy, what is the value of subject_tier when input.subject.account is "svc-unknown", and how does that produce a deny?
  2. Rewrite the membership check input.method in read_methods as prose. Why is in preferred over iterating read_methods by index?
  3. If you added "svc-export": "write" to account_tiers, would you also need to edit write_accounts? Explain why or why not.