Getting Started With Rego

Rego is the language used by OPA (Open Policy Agent) to write declarative, easily extensible policy decisions. This article should help you get started writing Rego policies that (1) work, and (2) are concise and readable.

Read the documentation first.

How are rules evaluated?

In its most basic form, every rule has a return value and some statements. Statements are either equality statements or assignment statements.

If the return value is ground (i.e a constant), the rule will return that value if all the statements in the body hold to be true. If any of the statements is false, the rule will return undefined unless there is a default value, in which case this is returned.

Assignments are always counted as true statements. Any variable on its own is also counted as a true statement (even if a list/set is empty, etc.). The only way to have a false statement is to have a false comparison

If the return value is a variable (as it is in a set definition rule, for example), the rule will evaluate all of the statements and will return whatever is assigned to the given variable at the end: f = fry { fry := 23 } will return 23. In this case, there is no situation in which the statements are false and thus adding a default has no effect.

One final layer of complexity is added by set rules. These can be queried with bound or unbound variables. If you query a set rule with an unbound, it will allow any value for that given variable in the output. If you query with a variable that has been defined, then it will be restricted to that value in the output. It is also possible to query with just the name of the rule in which case it is as if you queried with all unbound variables.

Example:

sites = [
{"name": "prod"},
{"name": "smoke1"},
{"name": "dev"}
]

If we query with unbound variables:

> q
["prod", "smoke1", "dev"]

If we query with bound variables:

> q["smoke2"]
undefined
> q["dev"]
"dev"

Once again, q["smoke2"] is undefined because there is no situation in which name = "smoke2" and the given query holds true.

I’m confused about syntax…

  1. Set Rule

This is not a function. A key difference is that set rules don’t take parameters. Instead of calling the input items parameters or args, I will refer to them as objects. The object referred to here is both an input and an output. The rule will output a set containing all values of the object that satisfy all the statements in the rule. You should thus use the object in your conditions to restrict the values in your output.

2. Object Definition Rule

These look like this:

If you don’t include a return value explicitly, it defaults to true

What’s the difference between := , == and = ?

This is possibly the most important thing to understand when writing Rego.

x := … creates a locally scoped x and requires that x has no value before that line in the rule. Even if you have a globally defined x you get a new variable.

x == … returns true if x is equal to the value given and requires that x already has a value before that line in the rule.

x = y does both functions. If x and y already have values, it acts as ==. If either does not, it acts as :=. If neither operand has a value, it throws an error. The order of operands on either side of the = does not matter.

If you write a rule using :=, then the order of the statements strictly matters. This is more of an imperative programming style. If you write a rule using =, the order does not matter since the engine will return true if it finds some combination of values which satisfies all the conditions. This is more of a declarative programming style. For example:

invalid { x == 5; x := input.a } # assigning to a bound value
also_invalid { x = 5; x := input.a } # assigning to a bound value

Why does the = operator behave in such a funky way? The idea behind declarative programming is that the programmer should only have to worry about higher level logic. Using the = operator allows you to write conditions without worrying about variable assignment; the compiler will do that all for you.

When should I use a rule or a function? Are the two interchangeable?

Rules are supersets of functions, although there are some situations in which using a function is preferable.

A function func(a,b) = c { //body } can always be replaced by a rule rule = c { //body }. The difference is that the invoking mechanism is different, with the function invoking syntax arguably cleaner: result := func(arg1, arg2) vs. result := rule with input.a as arg1 with input.b as arg2.

A function cannot be queried, so if you need to expose something as a policy definition, it should always be a rule. Anything that is a utility for other rules may be better served as a function.

How do I pass inputs/arguments around?

If you’re calling a function, it’s exactly what you would expect.

Rules expect their inputs to come from the input object. This is a global object. When you call a rule from another rule, you can:

  1. Replace the entire input document using the syntax with input as {"example": example}
  2. Selectively choose which portions of this document you wish to override/add to at any time using with input.[key] as [val]

Since the input object is global, any changes you make to it are also global. For example, invoking main with input.a as 1 and input.b as 1 will return false since the last expression is always false.

addition = x {
x := input.a + input.b
}
default main = false
main {
15 == equal with input.a as 6 with input.b as 9
input.a == input.b
}

If a rule accesses an key of the input object that does not exist it fails immediately, returning undefined (if no default exists).

Why is the syntax for evoking functions in the documentation so weird?

You can call functions exactly how you would expect:

result := function(arg1, arg2)

Why is my rule returning undefined?

All its statements are not true and you haven’t specified a default value. Specifying a default will likely get around this issue. See How are rules evaluated at the top of this page for a longer explanation.

Given a list, how can I tell whether “potato” is in it?

Enter wildcards.

list[_] == "potato"

How can I search through a list for a given item and store it somewhere?

default bopHasType5 = false
bopHasType5 {
list[i].name == "bop"
list[i].type == 5
}

The rule above will return true if there is an item in list with name “bop” and type 5. i gets stored as the index of the value that makes the statement true and can be referenced elsewhere throughout the rule.

How can I tell whether a set/array lst is empty?

You cannot do lst. Remember that variables always evaluate to true, regardless of their value.

You can do lst == [] or lst == set() , but that requires knowledge of the type.

The cleanest solution is just to do count(lst) == 0

Does Rego support higher order functions like mapping or filtering?

Essentially. Rego supports Python-esque list comprehensions for arrays, objects or sets. If you want to filter list by predicate then you can do, depending on whether your predicate is a rule or a function:

filtered := [f | f = list[_]; predicate with input.arg as f]
filtered := [f | f = list[_]; predicate(f)]

You can apply functions (but not rules) to the objects on the left of the pipe| . If you want to map func onto lst, you can thus do:

mapped := [func(x) | x = list[_]] 

Similar comprehensions exist for sets and objects.

How can I check a predicate like forall without explicit looping statements?

forall with pred is the logical equivalent of filtering for all items that do not satisfy pred, and then checking that none exist:

default is_admin_forall_items = false
is_admin_forall_items {
items_not_admin_of := [i | i = list[_]; is_admin(i)]
count(items_not_admin_of) == 0
}

Good luck!

Researcher @ Brown HCI, Brown CS '20