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:
Part | Definition |
---|---|
Predicate | The rule's name. Rule names are not meant to be unique. |
Parameters | Named 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 text | Part |
---|---|
within | Predicate |
(lower: Integer, upper: Integer, x: Integer) | Parameters |
if lower < x and x < upper | Expression |
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 Morganparent("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:
Query | Result |
---|---|
family String:Bernie String:Pat | family(String:Bernie, String:Pat) |
family String:Bernie String:Morgan | family(String:Bernie, String:Morgan) |
family String:Pat String:Morgan | no results |
family String:Bernie _ | family(String:Bernie, String:Pat) |
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 permissionsallow(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 undefined001: 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
- Resource blocks to begin creating your own types.
- Facts as data to begin creating your own types.
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.