Rego for PolicyArc
PolicyArc decides requests with Open Policy Agent (OPA), and OPA rules are written in Rego. You need a small part of the language to write a custom control. This page covers that part. Each example compiles with opa check, and the test at the end runs with opa test.
The Helm chart runs OPA 1.10.0, and our test instance ran 1.20.1. Start every file with import rego.v1. It turns on the keywords if, contains, in and every.
Packages
The first line names the package. A custom control lives under pbac.controls.
package pbac.controls.reports_read_only
import rego.v1
The product reads deny, expires_in_override, jit_single_use, rs_obligations, subject_obligations and rar_allow from each package under pbac.controls. Any other rule in your package is a helper for your own use.
Rules that build a set
A rule written with contains adds a member to a set. Every rule body that holds adds its member, and the set is the union. deny is built this way.
package pbac.controls.example_sets
import rego.v1
deny contains {"code": "reports_write_blocked"} if {
input.resource.type == "urn:example:reports"
input.action == "write"
}
deny contains {"code": "reports_unknown_client"} if {
input.resource.type == "urn:example:reports"
not input.context.client.client_id
}
The lines in a body are joined by AND. Each body is a separate OR. A body whose lines do not all hold adds nothing, so a rule that never matches never denies.
Rules with one value
A rule with := has one value. Add default to give it a value when no body holds.
package pbac.controls.example_values
import rego.v1
default tier := "untrusted"
tier := "trusted" if input.context.client.client_id == "reports-agent"
Without the default, tier is undefined for every other client. A line that tests an undefined value does not hold, so the body it is in adds nothing.
Reading data rows
A data row you write through the Admin API is served to OPA under its key, dots included. Read it with brackets and give the rule a default.
package pbac.controls.example_data
import rego.v1
default allowed_clients := []
allowed_clients := data["custom.reports_allowlist"].client_ids
Do not read a row with object.get(data, ...). That form refers to the whole data document, and opa check rejects your package as recursive. object.get on input is fine.
package pbac.controls.example_input
import rego.v1
department := object.get(input.context.client.software_statement, "department", "")
Looping and negation
some x in list holds for each member. not holds when the expression is false or undefined. every holds when the body holds for each member.
package pbac.controls.example_loops
import rego.v1
asks_for_reports if {
some r in input.resources
r.type == "urn:example:reports"
}
default allowed_clients := []
allowed_clients := data["custom.reports_allowlist"].client_ids
client_not_listed if not input.context.client.client_id in allowed_clients
all_reports if {
every r in input.resources {
r.type == "urn:example:reports"
}
}
client_not_listed holds when the row lists other clients, and also when the row does not exist, because allowed_clients then falls back to the empty list. Keep the default. Without it, a missing row leaves the list undefined, and not ... in over an undefined list does not hold. The rule would then let every client through.
Time
time.now_ns() is the current time in nanoseconds. time.clock turns it into hours, minutes and seconds in UTC.
package pbac.controls.example_time
import rego.v1
utc_hour := time.clock(time.now_ns())[0]
business_hours if {
utc_hour >= 9
utc_hour < 17
}
Testing
A test is a rule whose name starts with test_. Put tests in their own package, outside pbac.controls. Replace input, a data row or the clock with with.
package pbac.controls_test.example_time
import data.pbac.controls.example_time
import rego.v1
test_noon_is_business_hours if {
example_time.business_hours with time.now_ns as 1768478400000000000
}
test_three_in_the_morning_is_not if {
not example_time.business_hours with time.now_ns as 1768446000000000000
}
Save the rule and the test in one folder and run opa test . -v. Each test prints PASS or FAIL.
Next steps
- Write a policy puts these ideas into a control and deploys it.
- Test a policy and explain a denial builds test inputs from real requests.
Send feedback about this page