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:
- A read-tier or write-tier account may call a read method — check
subject_tier in {"read", "write"}andinput.method in read_methods. - Only a write-tier account may call a write method — check
input.subject.account in write_accountsandinput.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
- In the policy, what is the value of
subject_tierwheninput.subject.accountis"svc-unknown", and how does that produce a deny? - Rewrite the membership check
input.method in read_methodsas prose. Why isinpreferred over iteratingread_methodsby index? - If you added
"svc-export": "write"toaccount_tiers, would you also need to editwrite_accounts? Explain why or why not.