Sentinel
Language: Rules
Rules form the basis of a policy by representing behavior that is either passing or failing. 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:
Sentinel Playground
Loading the playground...
Press "Run" to get policy output
A rule contains a single expression. This can be a simple boolean
expression, or an expression representing the
discovery of a set of violations using more in-depth expressions like filter
and map
. You can split expressions
into multiple lines for readability, as you would with any other expression
within Sentinel.
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.
Making rules easier to understand
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 and homework is "")
}
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 understand, 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 and homework is "" }
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 and homework is "" }
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.
Non-Boolean Values
Sentinel supports non-boolean values in rules. This can be useful for passing more detail along to a calling integration when more detail is required than a boolean value would be able to communicate. This data shows up in the policy trace and can be utilized in different ways depending on the integration you are using Sentinel with.
Consider a different take on our policy above:
// A policy to check a set of scheduled days to determine what days you can't
// come out and play, based on the day not being a weekend day and there being
// homework to do.
param days
main = rule {
filter days as d {
d.day not in ["saturday", "sunday"] and
d.homework is not ""
}
}
When given a value in the set of days
that has a day
that is a weekday, and
homework
present, the policy will fail. Additional, when tracing was enabled,
the days that were detected as violating the policy would be given in the trace
for the main
rule.
Generally, for non-boolean values, a policy fails on non-zero data. See the
details on the main
rule for more details.
The result of a rule must be either a boolean, string, integer, float, list, or map. All other types will result in a runtime error.
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:
Sentinel Playground
Loading the playground...
Press "Run" to get policy output
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.