Overview of OPA #
The Probelm OPA Solves #
What is OPA #
Quiz on OPA and its Integrations #
OPA Features #
Quiz on OPA Features #
Future Reading #
Rego Expressions #
Single Values #
Rego Design Principles #
Syntax: Mirror declarative real-world policies #
99% of Rego statements are IF statements, like those found in PDF/email policies
allow {
user == "alice" # allow if user is alice
}
Semantics: Embrace hierarchical data #
Rego provides first-class support for navigating and constructing deeply nested data
input.token.claims[i].id
Algorithms: Optimize Performance Automatically #
Policy author is responsible for correctness. OPA is responsible for performance
Rego Overview #
When writing Rego you do two things:
- Write Rules that make policy decisions. A Rule is a conditional assignment.
Assignment | IF | Conditions |
---|---|---|
allow is true | IF | user is alice and action is read |
allow := true | IF | {user == “alice”; action == “read”} |
• value assignment • element assignment to a set • key assigned to a value • function call assigned to a result |
IF | • variable assignment • Reference, e.g. input.user • Equality or inequality • Function call • iteration |
- Organize Rules into Policies. A Policy is set of Rules with a hierarchical name.
Values & Variables #
Rego is a superset of JSON. Rego values are JSON values plust sets.
- String
- Number
- Boolean
- null
- Array (a list of values)
- Object (a dictionary mapping strings to values)
- Set (an unordered collection of distinct values)
Assignment (:=) assigns a variable to a value
s := "a string" #string
n := 17.35 #number
b := true #boolean
u := null #null
a := [1, 1, 2, 3] #array
d := {"key": "value", "user": "alice", "path": ["Pets", "dogs"]} #object
e := {1, 2, 3} #set
Variable are immutable. Do not assign them twice
x := 1
x := 2 # COMPILER ERROR
Input and Data Variables #
input is a global variable sotring the JSON object given to OPA
# OPA does this assignment for you
input := {
"metadata":{
"name": "netpol1",
"namespace": "dev"}
},
"spec": {...}
}
data is a global variable storing the external data given to OPA
# OPA manages this variable for you
data := {
"oncall" :{
"alice": { "level": 2},
"bob": { "level": 1},
"charlie": { "level": 1}
},
... # many other sources of data
}
Bracket Expression #
# object
obj := {
"user": "alice",
path: ["pets", "dogs"]
}
# array
arr := ["apple", "banana", "carrot", "apple"]
# set
st := {"apple", "banana", "carrot"}
Bracket [] inspects objects
obj["user"] # "alice"
key:= "user"
obj[key] # "alice"
Brackets [] inspect arrays (0-indexed arrays)
arr[2] # "carrot"
index := 2
arr[index] # "carrot"
Brackets [] inspect sets
st["banana"] # "banana"
key := "banana"
st[key] # "banana"
Bracket applies repeatedly
obj["path"][1] # "dogs"
Dot Expressions #
# object
obj := {
"user": "alice",
"path": ["pets", "dogs"],
"foo": {"bar": 7}
}
# array
arr := ["apple", "apple", "banana", "carrot"]
# set
st := {"apple", "banana", "carrot"}
Dot . is a shorthand for Bracket[]. Rego makes x.y into x[“y”]
obj.user # "alice"
obj["user"] # "alice"
obj.path[0] # "pets"
obj.foo.bar # 7
obj["foo"].bar # 7
Dot can be used only when the key is alpha-numeric starting with a letter. In practice, Dot is used only with objects, not arrays or sets.
arr.0 # COMPILER ERROR
arr[0] # "apple"
Examples to test your knowledge
x := "user"
obj[x] # Equivalent to obj["user"], which is "alice"
obj.x # Equivalent to obj["x"], which is missing
y := "foo.bar"
obj[y] # Equivalent to obj["foo.bar"], which is missing
obj.foo.bar # Equivalent to obj["foo"]["bar"], which is 7
Undefined #
# object
obj := {
"user": "alice",
"path": ["pets", "dogs"],
"foo": {"bar": 7}
}
# array
arr := ["apple", "apple", "banana", "carrot"]
# set
st := {"apple", "banana", "carrot"}
When a path is missing, result is Undefined–not an error.
obj.x # Undefined
obj.foo.x # Undefined
obj.x.y.z # Undefined
obj.path[47] # Undefined
count(obj.path[47]) # Undefined
v := obj.path[47] # v is Undefined
NOT turns UNDEFINED into true NOT turns false into true NOT turns everything else into undefined
not obj.x # true
not false # true
not 42 # undefined
not true # undefined
Check path existence by writing the path
# check is path exists
obj.foo.bar
# check if path does not exists
not obj.foo.x
Comparing and Constructing Values #
Equality Expressions #
Comparison operator (==) on scalars checks if values are equal
"apple" == "apple" # true
1 == 2 # false
Comparison (==) does recursive, semantic equality checks
[1, [2, 3]] == [1, [2, 3]] # true
{1, 3, 1, 4} == {4, 4, 1, 3, 1} # true
{1, 2, 3} == {2, 3, 4} # false
{"alice": 1, "bob": 2} == {"bob":2, "alice" : 1} #true
[{"alice": 1}, "bob": 2}, {3,4}] == [{"alice": 1}, "bob": 2}, {4,3}] #true
Unification operator (=) combines assignment (:=) and comparision (==) Unification assigns any unassigned variables so that the comparison returns true. Use it only when necessary. Prefer := and ==
[1, x] = [1, 2] # x is assigned to 2
[1, x] = [y, 2] # assigns x to 2 and y to 1
[1, x] = [2, y] # undefined
Built-in Expressions #
50+ builtin functions for comparison and construction: openpolicyagent.org/docs/latest/policy-reference/
- No mutation of arguments. Return new values instead
- No optional arguments, though can take objects/arrays/set as arguments.
- Can generate errors (e.g. division by zero)
Expressions: | … |
---|---|
Basic: | ==, !=, <, <=, >, >=, +, -, *, /, % |
Strings: | concatenate, lowercase, trim, replace, regexp, glob |
Arrays/Sets/Objects: | concatenate, slice, intersect, union, difference, remove, filter |
Aggregates: | count, sum. min, sort |
Parsing: | base64, url, json, yaml |
Tokens: | verification, decode, encode |
Time: | date, time, weekday, add |
Network CIDRs: | contains, intersects, expand |
Basic builtins are infix
x + (y * 3) > 5
Remaining builtns are functions
count(z) > 1
part := substring(w, 0, count(t))
Basic Rego Rules #
Boolean rules and evaluation #
Rego Overview #
When writing Rego you do two things.
- Write rules that make policy decisions. A Rule is a conditional assignment.
Assignment | IF | Conditions |
---|---|---|
allow is true | IF | user is alice and action is read |
allow := true | IF | {user == “alice”; action == “read”} |
• value assignment • element assignment to a set • key assigned to a value • function call assigned to a result |
IF | • variable assignment • Reference, e.g. input.user • Equality or inequality • Function call • iteration |
Boolean Rules #
request:
id: 123
method: GET
path: "/api/v1/products"
host: "192.168.1.1"
protocol: "HTTP/1.1"
token:
user: alice
roles:
- manager
- engineering
Boolean rules are IF statements that assign a variable to true or false. Neither allow nor deny are keywords; they ar eboolean variables.
# Variable 'allow' is assigned the value 'true' IF ...
allow = true{
...
}
By default a rule assigns the value true
# the following 2 rules are equivalent
allow = true {
...
}
# vs.
allow {
...
}
# OPA knows what you mean
The IF part (rule body) is a collection of (i) assignments and (ii) expressions. The IF part is an AND. All assignments and expressions must succeed for the IF to succeed.
# alice can read everything
allow = true { # allow if true if ...
input.request.token.user == "alice" # user is alice AND
input.request.method == "GET" # method is GET
}
Rule evaluation #
request:
id: 123
method: GET
path: "/api/v1/products"
host: "192.168.1.1"
protocol: "HTTP/1.1"
token:
user: alice
roles:
- manager
- engineering
A successful rule evaluation
allow = true{
input.request.token.user == "alice" # true AND
input.request.method == "GET" # true
}
# allow evaluates to true
A unsuccessful rule evaluation
allow = true{
input.request.token.user == "alice" # true AND
input.request.method == "PUT" # false
}
# allow evaluates to undefined. This is because allow is ONLY set to true WHEN the two conditions in the body are met. Since they are not met allow is not defined.
An unsuccessful rule evaluation with undefined
allow = true{
input.request.method == "GET" # true AND
startswith(input.food.bar, "baz") # undefined
}
# allow evaluates to undefined
Multiple Rules #
request:
id: 123
method: GET
path: "/api/v1/products"
host: "192.168.1.1"
protocol: "HTTP/1.1"
token:
user: alice
roles:
- manager
- engineering
Multiple rules give logical OR.
is_read{
input.request.method == "GET"
}
is_read{
input.request.method == "HEAD"
}
Rule order is irrelevant.
OPA could decide to evaluate ALL rules that are pertinent to the query. OPA could decide to terminate early but will return the same result as if it evaluated all rules.
For priority evaluation, use ELSE keyword. But use it sparingly because it disables optimizations. Prefer instead to make rule bodies mutually exclusive.
Under- and Over- assignment #
request:
id: 123
method: GET
path: "/api/v1/products"
host: "192.168.1.1"
protocol: "HTTP/1.1"
token:
user: alice
roles:
- manager
- engineering
If no rules succeed, a scalar variable’s value is undefined
is_read{ ... }
is_read{ ... }
is_read{ ... }
DEFAULT sets a value when no rules succeed
default is_read = false # use single equals here
Multiple rules yielding different assignments produces an error. Avoid by making rule bodies mutually exclusive
foo = true {true}
foo = false {true} # RUNTIME ERROR
The not operator needs to be the outermost operator in a rule condition
Rule chaining and non-boolean rules #
Rule Chaining #
request:
id: 123
method: GET
path: "/api/v1/products"
host: "192.168.1.1"
protocol: "HTTP/1.1"
token:
user: alice
roles:
- manager
- engineering
Rules can be used by other rules. Recommend using helpers for readability and modularity.
allow{
action_is_read
user_is_authenticated
}
action_is_read { ... }
user_is_authenticated { ... }
Note: Neither allow nor deny are keywords. They are just variables. Recursion is forbidden
allow{
action_is_read
user_is_authenticated
}
user_is_authenticated{
allow # COMPILER ERROR: recursion is forbidden
}
Rule Chaining for AND/ORs #
Policy: allow IF action is a read and user is authenticated or path is the root
# Helpers
action_is_read{ ... }
user_is_authenticated{ ... }
path_is_root{ ... }
Option 1: ((aciton_is_read AND user_is_authenticated) OR (action_is_read AND path_is_root))
allow{
action_is_read
user_is_authenticated
}
allow{
action_is_read
path_is_root
}
Option 2: (action_is_read AND (user_is_authenticated OR path_is_root))
allow{
action_is_read
safe # new helper
}
safe{
user_is_authenticated
}
safe{
path_is_root
}
Non-boolean Rules #
Variables can be assigned any Rego Value
code = 200 {
allow
}
code = 403 {
not allow
}
Values can be computed
port_number = result{
values := split(input.request.host, ":")
result:= to_number(values[1])
}
Commonly, multiple values are returned via an object
authz = result{
result :={
"allowed": allow,
"code" : code
}
}
Rule bodies are optional
# all of these are equivalent
pi = x {
x := 3.14
}
pi = 3.14 {
true
}
pi = 3.14
Policy Decisions #
A POLICY DECISION in Rego is the value of a variable. Caller asks for the value of a variable.
allow { ... }
code = 200 {
allow
}
code = 403 {
not allow
}
authz = {
"allowed": allow,
"code" : code
}
A POLICY DECISION in Rego is the value of a variable. Caller asks for the value of a variable.
POST /v1/data/<policypath>/allow <input>
=> { "result": true}
POST /v1/data/<policypath>/code <input>
=> { "result": 200}
POST /v1/data/<policypath>/authz <input>
=> { "result": { "allowed": true, "code": 200 } }
A Common Use Case: JWTs #
request:
id: 123
method: GET
path: "/api/v1/products"
host: "192.168.1.1"
protocol: "HTTP/1.1"
jwt: eyAOWIHDOAWIHD...
JSON Web Token (JWTs) often contain end-user information
user:alice
roles:
- manager
- engineering
Once decoded, the JWT is another JSON object
claims = payload{
# verify the token (key can be pulled from environment)
io.jwt.verify_hs256(input.request.jwt, "B4BD1203109h9ahd9...")
# decode the token
[header, payload, signature] := io.jwt.decode(input.request.jwt)
}
Use the JWT contents to make decisions
# allow alice to do everything
allow{
claims.user == "alice"
}
Unit tests and test coverage #
package main
import data.policy.role
import future.keywords.if
allow_review := true if { # allow a customer with reputation >=0 to review
input.role == "customer"
input.reputation >= 0
}
allow_delete := true if { # allow a moderator to delete
role.is_moderator == true
}
### TEST CASES ##
# Typical form
test_NAME if {
EXPECTATION_CONDITION with input as TESTING_INPUT
}
test_allow_review if { # PASS
allow_review == true with input as {"role" : "customer", "reputation": 0}
}
test_disallow_review_non_customer if{ # FAIL this is because allow_reivew can be set to false or undefined value.
allow_review == false with input as {"role" : "foo", "reputation": 0}
}
# to fix that we use NOT
test_disallow_review_non_customer if{ # PASS
not allow_review with input as {"role" : "foo", "reputation": 0}
}