»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:

is_sunny     = rule { weather is "sunny" }
is_wednesday = rule { day is "wednesday" }

main = rule {
    is_sunny and
    is_wednesday
}

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.

»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 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:

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.