title: 'Rego & Fugue Rules 101' author: 'Jasper Van der Jeugt' patat: incrementalLists: true eval: rego: command: fregot repl -v0 sql: command: sqlite3 javascript: command: node bash: command: bash eval: command: bash fragment: false replace: true ...
-
Let's learn Rego
- We'll compare Rego to a few other languages, including ones which you may be familiar with including JavaScript and SQL.
- Gradually we'll focus deeper and deeper on what sets Rego apart.
- Packages, input, rules
-
Fugue rules
- Simple rules
- Advanced rules
- Custom error messages
-
Interactive part
-
Advanced Rego topics
- Comprehensions
- Unification
We're using fregot:
https://github.com/fugue/fregot
Releases > darwin binary works on Mac OS X.
. . .
Please stop me at any time to ask questions.
https://github.com/jaspervdj-luminal/rego-brownbag
tree
Scalars: Numbers, strings, booleans, null
1 + 2
"Hello world"
false
null
Compound objects: Arrays
arr = [1, 2, 3, 4]
count(arr) # ?
array.slice(arr, 0, 2) # ?
arr[0] # ?
sum(arr) # ?
Compound objects: Objects
tags = {"owner": "Finance"}
tags.owner # ?
Compound objects: Sets
allowed_ports = {80, 443, 8080}
count(allowed_ports) # ?
allowed_ports[80] # ?
intersection({allowed_ports, {8080}}) # ?
Built-in functions
re_match("^::", "::/0") # ?
time.now_ns() # ?
base64.encode("Hello world") # ?
Full list at:
https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions
Find natural numbers that add up to 5:
const numbers = [1, 2, 3, 4, 5];
var pairs = [];
for (const x of numbers)
for (const y of numbers)
if (x < y && x + y == 5)
pairs.push([x, y]);
console.log(pairs); // ?
Ok so Rego is actually not like JavaScript:
- Immutable (cannot modify
pairs
after assignment)- Obviously has an effect on built-ins
- Functional Programming
- Not Turing Complete
- Guaranteed to terminate
- But not that hard to write a query that would take longer than the age of the universe to compute
- Declarative
- Order isn't that important
- Non Turing-Completeness allows for tricks™
numbers = [1, 2, 3, 4, 5]
pairs[[x, y]] {
x + y = 5
x = numbers[i]
y = numbers[j]
i < j
}
pairs[_] # ?
CREATE TABLE numbers (value INT);
INSERT INTO numbers (value) VALUES (1),(2),(3),(4),(5);
SELECT n1.value, n2.value
FROM numbers AS n1 JOIN numbers AS n2
WHERE n1.value < n2.value AND n1.value + n2.value = 5;
...but with first-class JSON support and friendlier syntax
jq_script='
.[] as $x | .[] as $y |
select($x<$y) | select($x+$y==5) |
[$x, $y]'
echo '[1, 2, 3, 4, 5]' | jq -c "$jq_script"
...but sometimes you can actually read the code
- Operates on JavaScript values (with the addition of sets)
- Syntax looks like JavaScript
- But it's not an imperative language
- Closer to datalog, SQL, jq, prolog
In Rego, everything is immutable, which means that almost* everything is data:
data
|-package a
| |-package a.x
| |-rule a.x.foo
|-package b
|-rule b.bar
In fregot repl
, you're always in the "open" package:
:open a.x
foo = 1
data # ?
data.a.x.foo # ?
cat rules/rule0.rego
fregot eval 'data' rules/rule0.rego | jq '.'
All your rules are just data!
Working with the Rego tree:
import data.rules.aws.ports_by_account
foo = ports_by_account.allowed_ports
. . .
import data.rules.aws
foo = aws.ports_by_account.allowed_ports
. . .
import data.rules as top
foo = top.aws.ports_by_account.allowed_ports
. . .
foo = data.rules.aws.ports_by_account.allowed_ports
Aside from data.
, there's input.
which holds the input document.
You can set this by using --input foo.json
on the command line or by using
:input
in fregot repl
:
:input inputs/resource1.json
input.id
Two JSON trees:
-
input
is the static input JSON document. In the case of Fugue Rules, this is either a single resource or a collection of resources. -
data
is derived from input through the rules you load, but it is also just a static document.
Rules and functions.
package rules.aws.ports_by_account
resource_type = "aws_security_group"
Rules can have bodies. A body is a list of queries.
cat rules/rule1.rego
:input inputs/resource1.json
:load rules/rule1.rego
account_id # ?
A query is a pure function that takes the current environment, and produces a list of new environments:
numbers = ["zero", "one"]
x = numbers[i]; [i, x] # ?
A query is a pure function that takes the current environment, and produces a list of new environments and results:
numbers = ["zero", "one"]
x = "one"
x = numbers[i]; [i, x] # ?
A query is a pure function that takes the current environment, and produces a list of new environments and results:
numbers = ["zero", "one"]
i = 3
x = numbers[i]; [i, x] # ?
A query is a pure function that takes the current environment, and produces a list of new environments and results:
numbers = ["zero", "one"]
x = numbers[i]; [i, x]
{} -> [{i = 0, x = "zero"}, {i = 1, x = "one"}]
{x = "one"} -> [{i = 1, x = "one"}]
{i = 3} -> []
Rules can have bodies. A body is a list of queries.
cat rules/rule1.rego
rule = result_1 {
query_1_a
not query_1_b
}
rule = result_2 {
query_2_a
not query_2_b
}
Read as:
IF query_1_a AND not query_1_b THEN rule = result_1
IF query_2_a AND not query_2_b THEN rule = result_2
rule = result_1 {
query_1_a
not query_1_b
} {
query_2_a
not query_2_b
}
Read as:
IF query_1_a AND not query_1_b THEN rule = result_1
IF query_2_a AND not query_2_b THEN rule = result_1
Alternatively:
IF (query_1_a AND not query_1_b) OR (query_2_a AND not query_2_b)
THEN rule = result_1
rule {
query_1_a
not query_1_b
}
Read as:
IF query_1_a AND not query_1_b THEN rule = true
rule = result_1 {
query_1_a
not query_1_b
}
default rule = result_2
Read as:
IF query_1_a AND not query_1_b THEN rule = result_1
ELSE rule = result_2
There are different kinds of rules:
- Complete rule (single result)
- Set rules (generate sets)
- Object rules (generate objects)
And then there are functions which are slightly different:
- Partial rules (aka functions)
All of these rules can have a body consisting of queries.
cat rules/rule2.rego
:input inputs/resource1.json
:load rules/rule1.rego
:load rules/rule2.rego
allowed_ports # ?
cat rules/rule3.rego
:input inputs/resource1.json
:load rules/rule1.rego
:load rules/rule3.rego
allowed_ports # ?
What makes functions special?
double(x) = y {
y = x + x
}
double(10) # ?
We cannot iterate over all "double" values.
What makes functions special?
double[x] = y {
nums = [1, 2, 3]
x = nums[_]
y = x + x
}
double[1] # ?
double[_] # ?
We can iterate over all "rule" values, because it must assign each variable
including x
.
What makes functions special?
double(x) = y {
y = x * x
}
Normal rules can be evaluated in different ways:
double[1]
: is1
a member of the rule? Give me it's value.double[x]
: solve forx
.double
: give me the entire object.
Functions can only be evaluated in a single way:
double(2)
: What is double of2
?
Tests are just normal rules that start with test_
.
test_double {
double(1) == 2
}
Just run fregot test [all input files]
and you're done.
fregot test aws/ azurerm/ fugue/
passed: 630, failed: 0, errored: 0
We've seen that Rego is really just a language for producing JSON based on an input document, so any "engine" using Rego relies on conventions.
:input inputs/resource1.json
input.ingress # ?
Conventions:
- There must be a
resource_type
rule (string
) - There must be a
deny
rule (bool
)
cat rules/rule4.rego
Evaluation
fregot eval 'data' rules/rule4.rego | jq '.'
Interactive evaluation with fregot repl
:
:load rules/rule4.rego
:input inputs/resource1.json
deny # ?
Conventions:
- There must be a
resource_type
rule (string
) - There must either be a
deny
rule (bool
)... - ...or an
allow
rule (bool
)... - ...or both.
Use whatever is easier!
Conventions for custom messages:
- There must be a
resource_type
rule (string
) - There must be a
deny
rule (set<string>
)
deny[msg] {
true
msg = "Not allowed"
}
deny #?
cat rules/rule5.rego
Conventions:
- There must be a
policy
rule (set<judgement>
)
You can use the data.fugue
API to do this:
fugue.resources("resource_type")
gives you resources of a typefugue.allow_resource(resource)
makes a valid judgementfugue.deny_resource(resource)
makes an invalid judgement
Evaluation
fregot eval -i inputs/everything.json \
'data.rules.aws.ports_by_account.policy' \
lib/fugue.rego rules/rule5.rego | jq '.'
Are advanced rules "better"?
. . .
No
. . .
Should I use a advanced rule?
- Yes/no decision about a single resource: No
- Anything else: Yes
Conventions:
- There must be a
policy
rule (set<judgement>
)
Full API:
fugue.resources(resource_type)
fugue.allow_resource(resource)
fugue.deny_resource(resource)
fugue.missing_resource(resource)
fugue.deny_resource_with_message(resource, msg)
fugue.missing_resource_with_message(resource, msg)
fugue.resource_types_v0
Put fregot
in your PATH
: https://github.com/fugue/fregot
Run commands from the root of this repository: https://github.com/jaspervdj-luminal/rego-brownbag
- Obtain input (this is the hard part)
- Write the rule (this is easy cruising)
- Write some tests (copy and paste)
We already have the input
---------------------------.
`/""""/""""/|""|'|""||""| ' \.
/ / / |__| |__||__| |
/----------=====================| easy
| \ /V\ / _. | crusing
|()\ \W/ /() _ _ |
| \ / / \ / \ |-( )
=C========C==_| ) |--------| ) _/==] _-{_}_)
\_\_/__.. \_\_/_ \_\_/ \_\_/__.__.
Let's look at a rule
head -n 3 rules/ports_by_account.rego
Evaluating with fregot repl --watch
:
:input inputs/resource2.json
:load rules/ports_by_account.rego
deny # ?
account_id # ?
:input inputs/everything.json
:load lib/fugue.rego
:open repl
aws_security_groups_by_env[env] = sgs {
security_groups = data.fugue.resources("aws_security_group")
env = security_groups[_].tags.env
sgs = {sg.id |
sg = security_groups[_]
sg.tags.env == env
}
}
aws_security_groups_by_env
:input inputs/everything.json
:load lib/fugue.rego
:open repl
aws_security_groups_by_env[env] = sgs {
security_groups = data.fugue.resources("aws_security_group")
env = security_groups[_].tags.env
sgs = [sg.id |
sg = security_groups[_]
sg.tags.env == env
]
}
aws_security_groups_by_env
:input inputs/everything.json
:load lib/fugue.rego
:open repl
aws_security_groups_by_env[env] = sgs {
security_groups = data.fugue.resources("aws_security_group")
env = security_groups[_].tags.env
sgs = {sg.id: sg |
sg = security_groups[_]
sg.tags.env == env
}
}
aws_security_groups_by_env
provider = "provider.aws.us-east-1"
region = r {
[_, _, r] = split(provider, ".")
}
region # ?