Polar Operators

Operators are used to combine terms in rule bodies into expressions.

Unification (=)

Unification is the basic matching operation in Polar. Two values are said to unify if they are equal or if there is a consistent set of variable bindings that makes them equal. Unification is defined recursively over lists: two lists unify if all of their corresponding elements unify.

The unification operator (=) checks if its left and right operands unify; for example, "a" = "a", x = "a", or ["a", "b"] = [x, "b"] where the variable x is either bound to "a" or unbound.

Note that in the case of String values, unification is case sensitive, e.g.:

  • "x" does not unify with "X"
  • User{"Alice"} does not unify with User{"alice"}

Conjunction (and)

The and operator is used to state that a pair of conditions in a rule's body must both hold. For example, the rule…


oso_employee(first, last) if
is_user(first, last) and
is_employee("Oso", first, last);

…will be satisfied if the person is a user and an Oso employee.

Disjunction (or)

The or operator will be true if either its left or its right operand is true. Disjunctions can always be replaced by multiple rules with identical heads but different bodies, but the or operator may help simplify writing rules with alternatives. For example:


is_user(first, last) if
oso_employee(first, last) or
is_guest(first, last);
# The `or` can be rewritten as a pair of rules:
is_user(first, last) if oso_employee(first, last);
is_user(first, last) if is_guest(first, last);

Negation (not)

The not operator is used to check that a certain fact does not exist. For example, you might use it to say that a user should only be allowed to perform an action if they are not banned, and that the policy grants them that permission.

This would look like below:


allow(user, action, resource) if
not is_banned(user) and
has_permission(user, action, resource);

Because negation can make logic harder to follow, Polar limits how it can be used:

  • You can only negate a single fact, not a compound expression (like one using and or or), or any expression that refers to another rule in your policy.
  • Any variable used inside a negated fact must also appear in a non-negated fact in the same rule.

The second rule exists to ensure that Polar always knows what values a variable could refer to. Negated facts don't introduce new values, they only check for the absence of something. So any variable used inside them must already be defined elsewhere in the rule.

These restrictions also make it possible for Polar to translate negated logic into efficient database queries. Internally, Polar compiles negation into SQL using NOT EXISTS subqueries. That means it must already know which values to check against before running the negated condition.

Despite these constraints, Polar supports a wide range of patterns using negation, including common use cases like:

  • Requiring a user to meet all required conditions (e.g., having every required role)
  • Allowing access from a parent resource unless it's overridden by more specific rules

These patterns are valid as long as all variables used in negated facts are introduced outside of them, and each negated expression consists of a single fact.

This design keeps policies understandable and ensures they can be evaluated safely and efficiently.

List membership (in)

The in operator can be used to iterate over lists of strings. An in operation looks like this:


x in ["a", "b", "c"]

The left operand, x, is a variable that will be unified with each element in the list. If the right operand is not a list of strings, the in operation will fail.

In the following example, the variable x will be bound to "a", "b", and "c", in turn, and then the x = "a" check will evaluate. This expression will only succeed for the first item in the list, "a".


x in ["a", "b", "c"] and x = "a"

The left operand does not need to be a variable. For example, the following expression will succeed twice since "a" is in the first and fourth positions in the list:


"a" in ["a", "b", "c", "a"]

Integer comparisons

The <, <=, >, and >= operators compare integer values. For example, if you have a fact expires_at(File:foo, 1670280790) indicating the time that the "foo" file expires:


expires_after_y2k38(resource) if
expires_at(resource, time) and time > 2147483647

matches operator

When writing complex rules you can assert that a variable is valid only for values of a specific type with the matches operator by writing:


<variable> matches <Type>

You typically only want use matches expressions when introducing a new variable in a rule, e.g. to join two rules.

For example, consider this policy snippet:


has_role(user: User, role: String, resource: Resource) if
group matches Group and
has_group(user, group) and
has_role(group, role, resource);

This introduces a variable named group, asserting that it must be of type Group. This rule is satisfied only if both the has_group and has_role facts can be satisfied with the same Group value.

Operator precedence

Polar operators are evaluated from highest precedence to lowest as follows:

  • in, matches (highest precedence)
  • =, <, <=, >, >=
  • not
  • and
  • or (lowest precedence)

For example, the following assertion passes:


actor User{}
e(user: User) if
a(user) or
b(user) and
c(user) or
d(user);
test "parent-child permissions" {
setup {
a(User{"alice"});
}
assert e(User{"alice"});
}

Oso evaluates the conditions as follows:


actor User{}
e(user: User) if
a(user) or# <-- true
b(user) and c(user) or# <-- false
d(user); # <-- false
test "parent-child permissions" {
setup {
a(User{"alice"});
}
assert e(User{"alice"});
}

That is, b(user) and c(user) is evaluated first. It returns false, but because a(user) is true, the entire condition evaluates to true or false or false, which returns true.

Overriding operator precedence with parentheses

You can use parentheses () to override Polar's default operator precedence. For instance, in the example above, if you instead wanted to state that e is true only if at least one of a and b is true, and at least one of c and d is true, you could rewrite the rule as follows:


actor User{}
e(user: User) if
(a(user) or b(user)) # <-- true
and
(c(user) or d(user)); # <-- false
test "parent-child permissions" {
setup {
a(User{"alice"});
}
assert e(User{"alice"});
}

Now the assertion fails, because the parentheses cause Oso to evaluate (a(user) or b(user)) and then (c(user) or d(user)) before and-ing the results. This evaluates to true and false, which evaluates to false.

Up next

  • Rules + facts to understand how to write Polar rules using operators.
  • Constants to see runtime-evaluated constants Polar offers

Talk to an Oso engineer

If you want to learn more about Polar, schedule a 1x1 with an Oso engineer. We're happy to help.

Get started with Oso Cloud →