How to express OR in Rego
One of the most common questions people new to Open Policy Agent (OPA) and Rego ask is about how to express logical “OR” in the language. While there is no “OR” operator, Rego has no shortage of ways to express that, with some being more obvious than others. In this blog, we’ll take a look at the most common ways to express OR, and weigh the virtues of each method against the others. Hopefully you’ll learn a few tricks along the way. One thing is certain — if you make it through to the end, there’s no way you’ll wonder how to express OR in Rego!
Logical AND
But first, let’s start with our logical sibling, AND.
As you know, Rego is all about rules, and evaluation of a single rule follows a simple pattern — if all the expressions inside of the rule body evaluate to true
, then the rule itself evaluates, and if the rule evaluates it will be assigned the value provided in the rule head. If no value is provided in the rule head, the rule will be assigned the default value of boolean true
.
package policy
import future.keywords.if
#
# valid_email will be assigned the value of the email variable if,
# and only if, all the expressions in the body evaluate
#
# rule head, name + (optional) assignment
valid_email := email if {
# fails if input.user.email is undefined
email := lower(input.user.email)
# fails unless email ends with hooli.com
endswith(email, "hooli.com")
}
Evaluation inside of a rule body — where all the expressions must be true — could easily be expressed using logical AND. In more imperative languages, we would likely use && between each expression in order to have evaluation stop at the first occurrence of a “falsey” expression:
var allow
if (expression1 && expression2 && expression3) {
// allow will only be assigned true if all
// expressions above are true
allow = true
}
Since a rule in Rego is essentially a conditional assignment — and often one that involves many conditionals — the “&&” is implied between each expression, and the above code would be described as:
# implicit assignment, same as: allow := true if {
allow if {
expression1
expression2
expression3
}
Not only did we not have to type out “&&” between the expressions — even the assignment to true
is implied when working with boolean rules.
Logical OR
Describing logical AND
is thus simple — so simple in fact that we don’t really have to describe it! How would we go on about describing logical OR
?
To start out, let’s consider the following example policy, where we try to determine if a user should be granted access to an endpoint under our /internal
resources:
package policy
import future.keywords.if
import future.keywords.in
default allow := false
allow if {
# User attempting to access internal resource
# i.e. something under /internal
input.request.path[0] == "internal"
# So user must work for AcmeCorp
endswith(input.user.email, "acmecorp.com")
# And user must have the "acmecorp-internal" role
"acmecorp-internal" in input.user.roles
}
There are a few places here where we could want to describe or, like if we wanted our rule to apply to more than one endpoint, or if we want to have a few more email domains that we should consider valid, or if we would want to allow more than a single role, or…
Let’s dive in to see what options we have for that. As it turns out, there are rather many!
Expressing OR using default assignment
The first way we’ll find is one you’re likely already familiar with, but might not have thought of as a way to express OR. In fact, we even make use of it in the example above!
All rules may be provided a default value, which means that if evaluation fails for any rule with the same name, the value assigned should be the default one. Another way to describe that could be “the value of the rule should be X if evaluation succeeds, or default value Y if it fails”. Default values must be literals known at compile time, since if they depended on e.g. attributes in the input, it would not be guaranteed that they could be assigned.
Use default assignment when you want to ensure that no matter what, a rule will always be assigned a value. While limited in scope, default value assignment is an idiomatic way to describe OR for any scenario where it’s applicable.
Expressing OR using helper rules
The next way to describe OR is by using helper rules. Rules are the building blocks of Rego policy, so that using rules to describe OR is a preferred solution in most cases may come as no surprise. How does it work then? If we were to replace the &&
from our previous pseudo-code example with a typical OR operator, like ||
:
var allow
if (expression1 || expression2 || expression3) {
// allow will only be assigned true if any
// of the expressions above are true
allow = true
}
What would the equivalent Rego look like using helper rules? Rather than evaluating expression 1, 2 and 3 inside of a rule body, we’ll make them distinct rules:
# implicit assignment, same as: allow := true if ...
allow if expression1
allow if expression2
allow if expression3
If any of the expressions in the example evaluate, the allow rule will evaluate too, and the result will be that allow is assigned the value true. Helper rules can be nested too, and in fact often need to be in order to express a more complex composition of AND/OR. Consider our example from before. How would we best add more email domains to the list of allowed ones? Using helper rules, we could create a new rule for that specific requirement:
package policy
import future.keywords.if
import future.keywords.in
default allow := false
allow if {
# User attempting to access internal resource
# i.e. something under /internal
input.request.path[0] == "internal"
# User must work for AcmeCorp, Hooli, or Lexcorp
valid_email
# And user must have the "acmecorp-internal" role
"acmecorp-internal" in input.user.roles
}
valid_email if endswith(input.user.email, "acmecorp.com")
valid_email if endswith(input.user.email, "hooli.com")
valid_email if endswith(input.user.email, "lexcorp.com")
We’ve now successfully externalized the email validation requirement into its own rule, and one which will be true if any of the conditions are true, or in other words, an OR-condition.
Using helper rules to describe OR is arguably the most idiomatic way of doing it in Rego, and should be your go-to solution for this problem whenever a simple default assignment won’t be enough.
Expressing OR using helper functions
Closely related to helper rules, helper functions are useful when the input may require some processing before a “branching”, or OR decision, needs to be made. In our previous example, our helper rule depended only on data available in the input. But consider a scenario like this:
package policy
import future.keywords.if
default allow := false
allow if {
idx := indexof(input.user.email, "@")
fullname := substring(input.user.email, 0, idx)
firstname := lower(split(fullname, ".")[0])
# Allow "joe" and "jane", but not anyone else. How?
}
Here we’ve processed the input in several steps, and eventually have extracted a first name which we’ll want to use to determine if access should be allowed. Needless to say, this isn’t a sensible policy for access control, but it’ll serve us well as an example. In this case, we might want to reuse the “fullname” and “firstname” variables later in the rule, so while using helper rules would be doable, it would be more convenient if we could branch off our “OR” evaluation inline. A helper function allows us to do just that, as we may pass any value we want to use as the basis of our OR-conditional.
package policy
import future.keywords.if
default allow := false
allow if {
idx := indexof(input.user.email, "@")
fullname := substring(input.user.email, 0, idx)
firstname := lower(split(fullname, ".")[0])
allowed_firstname(firstname)
}
# First name may be either "joe" or "jane" for function to evaluate
allowed_firstname(name) if name == "joe"
allowed_firstname(name) if name == "jane"
Multiple outputs
One thing to be aware of when using functions for branching is the risk of multiple outputs. This can happen in cases where you might have several conditions that are true, but render different results:
package play
import future.keywords.if
import future.keywords.in
# Both of the conditions could be true
validate_user(user) := "valid" if "admin" in user.roles
validate_user(user) := "invalid" if not user.email
valid := validate_user(input.user)
In the example above, the user would be both “valid” and “invalid” in case the user had the “admin” role, but not an email address. As this renders a runtime error, you’ll want to ensure helper functions that assist with OR can’t possibly render multiple outcomes for the same input.
Pattern matching function arguments
As a final foray on the topic of helper functions, Rego supports a limited form of equality-based pattern matching on function args. The “allowed_firstname” function we defined previously:
# First name may be either "joe" or "jane" for function to evaluate
allowed_firstname(name) if name == "joe"
allowed_firstname(name) if name == "jane"
Could be simplified to match on the argument directly, without using an intermediate variable:
# First name may be either "joe" or "jane" for function to evaluate
# No rule body needed as argument passed will be matched for equality
allowed_firstname("joe")
allowed_firstname("jane")
# This works with multiple arguments too, where only some are matched
# statically
alcohol_allowed("Sweden", age) if age > 18
alcohol_allowed("USA", age) if age > 21
alcohol_allowed(country, age) if {
# for any other country, we'll need to look up the rules
# for the country provided, perhaps by using http.send
}
To summarize: use helper functions to express OR whenever using helper rules will have you repeat logic needlessly, or when you’d like to branch off on a value you might want to reuse later in the body of a rule.
Using “else” to express OR
Using the else keyword to express or is arguably the option that syntactically feels most similar to other languages — almost all of them have an “else”-construct, after all. The name “else” implies an “if” preceding it, and that “if” would be the body of a rule or function, as covered above. What does else provide compared to just using helper rules or functions then? First, they’re not mutually exclusive, but often used together! The else construct differs in that it allows the policy author to control the flow of evaluation, as the “else” block is always evaluated after the expression or rule body preceding it.
# Expressions may be evaluated in any order
allow if expression1
allow if expression2
allow if expression3
# Expressions evaluated from top to bottom
allow if {
expression1
} else {
expression2
} else {
expresssion3
}
While controlling the flow of evaluation might sound appealing, there’s rarely a reason to do so. Rule evaluation in Rego is lightning fast, and optimizing for readability and idiomatic constructs is almost always a better idea than optimizing for performance. A notable exception would be when calling outlier functions, like http.send
, where you might want to put the expensive call in an else-block, and only have it evaluated if the data isn’t in the local cache.
Using “in” to express OR
Another trick for expressing OR when you have a simple comparison to make, such as “if the request method is HEAD or GET”, is to use the in
operator and a set to do a contains check:
allow {
# Simple way to "inline" an OR check —
# turn it into a "contains" problem
input.request.method in {“HEAD”, “GET”}
}
This “trick” is extremely useful for simple conditions, but if you find yourself repeating the same “in” checks over and over again in rules, consider using helper rules instead.
Using object-based branching to express OR
Similar to the using in
, maps allow you to branch on keys, with the added benefit of providing a value back. While you may not instinctively think of this as an “OR” condition, it’s branching nonetheless.
deny := message if {
code_reason_map := {
400: "Bad request",
404: "Not found",
500: "Internal server error",
}
message := code_reason_map[status_code]
}
The deny rule above naturally responds with “Bad request” if the status code is 400, or “Not found” if it’s 404, or…
Using maps for branching isn’t too common in Rego, but is occasionally useful for situations where both the condition (key) and the outcome (value) may be described in a single object.
Using “object.get” to express OR
Wow, so many ways to describe OR! And we still have some more to cover. Next up is the object.get
built-in function. This seems to be one of the most popular solutions for people new to Rego — likely because it doesn’t make use of any special language constructs. The object.get
function accepts an object (duh!), and a path for which to retrieve a value in that object. Most importantly for the purpose of expressing OR is however its third argument, which is the default value to return if no value was found on the provided path.
allow if {
# return input.user.name, or "anyomous" if the lookup fails
user := object.get(input, ["user", "name"], "anonymous")
user != "anonymous"
# ... more conditions
}
Using object.get
is occasionally useful for inlining an OR condition with an attribute lookup — and even rules may be queried this way! However, while it may feel natural for someone new to Rego, the inverse is likely true as well — for people experienced writing Rego, it doesn’t feel as idiomatic as many of the other options.
Using comprehensions to express OR
One construct that you’ll see fairly often in languages like Javascript is assignment like this:
arr = my_array || []
This is commonly used to “guard” an assignment from possibly being assigned an undefined value, and instead (i.e. OR) assign an empty array (or whatever default value is the most appropriate) in order to skip subsequent checks for undefined. Rego doesn’t treat undefined as a value, but rather as a signal to stop evaluation. Using comprehensions, we may still achieve something similar, as undefined inside of a comprehension merely stops evaluation in the comprehension, and not in its outer scope:
arr := [x | some x in input.my_array]
In the assignment above, “arr” will be the result of iterating over all the items in “input.my_array”. However — even if input.my_array is undefined, the result of the comprehension would be an empty array. Using comprehensions in such a way therefore allows both expressing “OR”, and to tackle undefined, in a succinct manner.
For simple inlined OR checks where undefined should not stop evaluation, comprehensions are occasionally useful. For most cases, you’ll be better off using a helper rule or function.
Wrapping up, and the future of OR
While OPA doesn’t have a dedicated OR operator, we’ve hopefully established by now that there’s no lack of methods for expressing or in the language. Branching happens in so many places, and as we’ve seen — sometimes without us really thinking about it as such. Which one should you use then? That’s likely a decision best made by yourself and the team you share policies with, but if you’re looking to write as idiomatic Rego as possible:
- Prefer expressing OR using helper rules and default assignment.
- Use helper functions when you need to pass a value from the body of a rule.
- Use
in
when you want to express a one-off OR condition of some known values. - Use object based branching and object.get sparingly, and comprehensions for when you want to transform a possible undefined into an empty collection.
If you’d like to ensure the policies in your team or organization follow the most idiomatic patterns, for OR expressions and much more, make sure to check out Regal, the new linter for Rego by Styra.
Finally, an open issue in the OPA backlog entertains the idea of adding an OR operator. Made it this far and still think there should be more ways to express OR? Make sure to upvote the issue!
If you’d like to discuss the many ways to express OR in Rego, or anything else related to OPA and Styra, join the discussion in the Styra Community Slack. If you want to learn more about OPA and Rego, make sure to check out the Styra Academy.