Polar rules and facts

The defining feature of any logic programming language is its ability to let programmers encode domain knowledge into sets of rules, which form relationships that provide the ability to draw inferences.

As we'll see, rules have an optional conditional clause expressing when the rule is satisfied. If a rule does not have a conditional clause, we call it a fact because it's unconditionally true.

When evaluating rules, Polar relies on other rules you define, as well as data stored as facts outside your policy. For more details, see fact data.

Construction

Polar rules are defined in the body of your policy and consist of the following parts:

PartDefinition
PredicateThe rule's name. Rule names are not meant to be unique.
ParametersNamed types whose values may unify this instance of the rule.
Expression(Optional) A conditional boolean expression evaluated to determine if the parameters satisfy the rule. If no expression is specified, any value that unifies with the parameters satisfies the rule.

Consider this trivial rule:


within(lower: Integer, upper: Integer, x: Integer) if lower < x and x < upper;

This decomposes into the following parts:

Rule textPart
withinPredicate
(lower: Integer, upper: Integer, x: Integer)Parameters
if lower < x and x < upperExpression

Evaluation

Polar evaluates rules' expressions when:

  • Another rule being evaluated refers to this rule
  • Users issue queries referring to this rule's predicate directly

To evaluate the expression, Polar attempts to find a set of conditions for which the expression returns true. If it finds a set of facts, it returns the conditions under which the rule is true; otherwise, the empty set.

To find the set of satisfying conditions, Polar uses rules from your policy, as well as any fact data.

Simple example

Given an environment with the following facts:


# Fact: Bernie has two parents, Pat and Morgan
parent("Bernie", "Pat");
parent("Bernie", "Morgan");

Given this rule:


# Rule: A parent and their children are considered family.
family(a: String, b: String)
if parent(a, b) or parent(b, a);

These queries generate these responses:

QueryResult
family String:Bernie String:Patfamily(String:Bernie, String:Pat)
family String:Bernie String:Morganfamily(String:Bernie, String:Morgan)
family String:Pat String:Morganno results
family String:Bernie _family(String:Bernie, String:Pat)
family(String:Bernie, String:Morgan)
family String:K _no results

For more complex and robust examples, see most other policy examples in the documentation. For example, RBAC: Resource-Specific Roles.

Typing variables in rules

When writing complex rules you will likely find yourself in need of introducing new variables to join rules together. We recommend explicitly annotating these variables' types using the matches keyword. For more details, see Operators: matches.

Literal parameters

Rules may be stated in terms of literal values, in which case the rule is only satisfied when the value unifies with the parameter's literal.

For example:


cities_to_visit("Phoenix", season: String)
if is_cold(season);

Would behave as if the rule were written as:


cities_to_visit(city: String, season: String)
if city = "Phoenix" and is_cold(season);

Facts: rules without expressions

You can write rules without expressions (i.e. not including the clause beginning with if); we refer to these as facts. In this case, any reference to the rule whose values unify with the parameters evaluates to true.

When writing facts, you might want to consider whether the fact is best served being stored in your policy, or whether you should store it as fact data.

Facts in policies

Each environment in Oso Cloud hosts a single Polar policy and all rules are evaluated within Oso cloud using any data that are available to it.

This means that any policy changes require re-deploying the entire policy, which requires administrator access to your Oso Cloud account.

Because of this, we expect facts in policies to be long-lived elements of your authorization pipeline.

Facts as data

Oso offers a variety of ways to store data as facts outside of your policy, which are detailed in Facts as data.

How exactly facts change depends on how they're stored, but changes to facts that affect authorization decisions never require re-deploying your policy.

It's instructive to think of facts as more dynamic because e.g. Oso clients can add or delete facts at any time.

Default & custom allow rules

If and only if your policy does not contain any rules with the predicate allow, Polar includes the following rules:


allow(actor, action, resource) if has_permission(actor, action, resource);
# For global permissions
allow(actor, action) if has_permission(actor, action);

We introduce this default rule because all authorization APIs (e.g. authorize, list) query allow.

In contrast, you can create your own custom rule with the allow predicate, which all authorization APIs will automatically use. This simplifies adding logic that applies to all queries, e.g. in the case of impersonation.

Singletons

If a variable occurs only once, then its value can't be used for anything. Such variables are called singletons, and Polar will warn you if they occur in a rule. For example, if you try to load the rule…


user(first, last) if person("George", last);

…you'll see the following message:


Singleton variable first is unused or undefined
001: user(first, last) if person("George", last);
^

The reason these warnings are important is that, as in this case, they usually indicate logical errors. Here, the error is forgetting to use the first variable, and instead using a literal string ("George") in the call to the person rule.

Singleton variables can be seen as wildcards: their values depend on nothing else in the expression and therefore can be anything. In the example above, first matches any value as long as person("George", last) results in a match.

If you wish to keep an unused parameter in a rule, you can suppress the singleton variable warning by starting your variable's name with an _ (underscore), e.g., change first to _first in the example above. The underscore makes explicit that the variable can match any value.

A variable named just _ (a single underscore) is called an anonymous variable, and it is always a singleton but will never generate a warning. Each occurrence is translated into a fresh variable, guaranteed not to match any other variable. You may therefore have as many anonymous variables in a rule as you like and each will be unique. It's up to you whether to use an anonymous variable or an underscore-prefixed singleton with a more descriptive name.

Polymorphism

Polar's type system is polymorphic, which lets you write more flexible and extensible rules. This is an advanced feature, so not all users have need to use it explicitly. For details, see the documentation on extends.

Up next

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 →