Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Add section on how to express "FOR ALL" in Rego #1527

Merged
merged 1 commit into from
Jun 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/content/how-do-i-write-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,98 @@ The result:
+-----------+------------------------+
```

## Universal Quantification (FOR ALL)

Like SQL, Rego does not have a direct way to express _universal quantification_
("FOR ALL"). However, like SQL, you can use other language primitives (e.g.,
[Negation](#negation)) to express FOR ALL. For example, imagine you want to
express a policy that says (in English):

```
There must be no apps named "bitcoin-miner".
```

A common mistake is to try encoding the policy with a rule named
`no_bitcoin_miners` like so:

```ruby
no_bitcoin_miners {
app := apps[_]
app.name != "bitcoin-miner" # THIS IS NOT CORRECT.
}
```

It becomes clear that this is incorrect when you use the [`some`](#some-keyword)
keyword, because the rule is true whenever there is SOME app that is not a
bitcoin-miner:

```ruby
no_bitcoin_miners {
some i
app := apps[i]
app.name != "bitcoin-miner"
}
```

You can confirm this by testing the rule inside the REPL:

```ruby
> no_bitcoin_miners with apps as [{"name": "bitcoin-miner"}, {"name": "web"}]
true
```

The reason the rule is incorrect is that variables in Rego are _existentially
quantified_. This means that rule bodies and queries express FOR ANY and not FOR
ALL. To express FOR ALL in Rego complement the logic in the rule body (e.g.,
`!=` becomes `==`) and then complement the check using negation (e.g.,
`no_bitcoin_miners` becomes `not any_bitcoin_miners`).

For this policy, you define a rule that finds if there exists a bitcoin-mining
app (which is easy using the `some` keyword). And then you use negation to check
that there is NO bitcoin-mining app. Technically, you're using 2 negations and
an existential quantifier, which is logically the same as a universal
quantifier.

For example:

```ruby
no_bitcoin_miners_using_negation {
not any_bitcoin_miners
}

any_bitcoin_miners {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use some here for additional clarity

some i
app := apps[i]
app.name == "bitcoin-miner"
}
```

```ruby
> no_bitcoin_miners_using_negation with apps as [{"name": "web"}]
true
> no_bitcoin_miners_using_negation with apps as [{"name": "bitcoin-miner"}, {"name": "web"}]
undefined
```

> The `undefined` result above is expected because we did not define a default
> value for `no_bitcoin_miners_using_negation`. Since the body of the rule fails
> to match, there is no value generated.

Alternatively, we can implement the same kind of logic inside a single rule
using [Comprehensions](#comprehensions).

```ruby
no_bitcoin_miners_using_comprehension {
bitcoin_miners := {app | app := apps[_]; app.name == "bitcoin-miner"}
count(bitcoin_miners) == 0
}
```

> Whether you use negation or comprehensions to express FOR ALL is up to you.
> The comprehension version is more concise and does not require a helper rule
> while the negation version is more verbose but a bit simpler and allows for
> more complex ORs.

## Modules

In Rego, policies are defined inside *modules*. Modules consist of:
Expand Down
31 changes: 31 additions & 0 deletions docs/content/language-cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ not obj.foo.bar.bar
# check if "foo" belongs to the set
a_set["foo"]

# check if "foo" DOES NOT belong to the set
not a_set["foo"]

# check if the array ["a", "b", "c"] belongs to the set
a_set[["a", "b", "c"]]

Expand Down Expand Up @@ -132,6 +135,34 @@ foo[k1] == foo[k2]; k1 != k2
foo[k].bar.baz[i] == 7; foo[k].qux > 3
```

#### For All

```ruby
# assert no values in set match predicate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/match predicate/make function f true

count({x | set[x]; f(x)}) == 0

# assert all values in set make function f true
count({x | set[x]; f(x)}) == count(set)

# assert no values in set make function f true (using negation and helper rule)
not any_match

# assert all values in set make function f true (using negation and helper rule)
not any_not_match
```

```ruby
any_match {
set[x]
f(x)
}

any_not_match {
set[x]
not f(x)
}
```

## Rules

In the examples below `...` represents one or more conditions.
Expand Down