Sentinel
Language: Rules
Rules form the basis of a policy by representing behavior that is either passing or failing (true or false). A policy can be broken down into a set of rules. Breaking down a policy into a set of rules can make it more understandable and aids with testing.
An example is shown below:
is_sunny = rule { weather is "sunny" }
is_wednesday = rule { day is "wednesday" }
main = rule {
is_sunny and
is_wednesday
}
A rule contains a single boolean expression. This boolean expression can be split into multiple lines for readability as shown in the example above. Boolean expressions should only be split into multiple lines after the operator ("and", "or", etc.).
A rule is also lazy and memoized. Lazy means that the rule is only evaluated when it is actually required, not at the point that it is created. This is covered in more detail in the lazy section. Memoized means that the value is only computed once and then saved. Once a rule is evaluated, the result of it is reused anytime the rule is referenced again.
Easing Understandability
Rules are an abstraction that make policies much more understandable by making it easier for humans to see what the policy is trying to do.
For example, consider the policy before which doesn't abstract into rules:
main = rule {
((day is "saturday" or day is "sunday") and homework is "") or
(day in ["monday", "tuesday", "wednesday", "thursday", "friday"] and
not school_today)
}
Assume that day
, homework
, and school_day
are available.
In plain english, this policy is trying to say: you can play with your friends only on the weekend as long as there is no homework, or during the week if there is no school and no homework.
Despite the relatively simple nature of this policy (a few lines, about 5 logical expressions), it is difficult to read and undertand, especially if you've not seen it before.
The same policy using rules as an abstraction:
is_weekend = rule { day in ["saturday", "sunday"] }
is_valid_weekend = rule { is_weekend and homework is "" }
is_valid_weekday = rule { not is_weekend and not school_today }
main = rule { is_valid_weekend or is_valid_weekday }
By reading the names of the rules, its much clearer to see what each individual part of the policy is trying to achieve. The readability is improved even further when adding comments to the rules to explain them further, which is a recommended practice:
// A weekend is Sat or Sun
is_weekend = rule { day in ["saturday", "sunday"] }
// A valid weekend is a weekend without homework
is_valid_weekend = rule { is_weekend and homework is "" }
// A valid weekday is a weekday without school
is_valid_weekday = rule { not is_weekend and not school_today }
main = rule { is_valid_weekend or is_valid_weekday }
"When" Predicates
A rule may have an optional when
predicate attached to it. When this is
present, the rule is only evaluated when the predicate results in true
.
Otherwise, the rule is not evaluated and the result of the rule is always
true
.
"When" predicates should be used to define the context in which the rule
is semantically meaningful. It is common when translating human language
policy into Sentinel to have scenarios such as "when the key has a prefix
of /account/
, the remainder must be numeric." In that example, when the
key does not have the specified prefix, the policy doesn't apply.
Assume you have rules is_prefix
and is_numeric
to check both the
conditions in the example above. An example is shown with and without
the "when" predicate:
example_no_when = rule { (is_prefix and is_numeric) or not is_prefix }
example_when = rule when is_prefix { is_numeric }
The rules are equivalent in behavior for all values of is_prefix
and
is_numeric
, but the second rule is more succint and easier to understand.
Testing
To verify a policy works correct, the built-in Sentinel test framework uses rules as the point where you can assert behavior. You say that you expect certain rules to be true or false. By doing so, you ensure that the policy results in the value you expect using the rule flow that you expect.
Therefore, in addition to readability, we recommend splitting policies into rules to aid testability.
Lazy and Memoized
A rule is lazy and memoized.
Lazy means that the rule is only evaluated when it is actually required, not at the point that it is created. And memoized means that the value is only computed once and then saved. Once a rule is evaluated, the result of it is reused anytime the rule is referenced again.
Both of these properties have important implications for Sentinel policies:
Performance: Rules are a way to improve the performance of a Sentinel
policy. If you have a boolean expression that is reused a lot, a rule will
only be evaluated once. In the example shown above, is_weekend
is used
multiple times, but will only have to be evaluated once.
Behavior: Rules accessing variables see the value of those variables at the time they are evaluated. This can lead to surprising behavior in some cases. For example:
a = 1
b = rule { a == 1 }
a = 2
main = b
In this example, main
will actually result to false
. It is evaluated
when it is needed, which is when the policy executes main
. At this point,
a
is now 2 and b
has not been evaluated yet. Therefore, b
becomes false
.
All future references to b
will return false
since a rule is memoized.