Rego Cheat Sheet
Contributors: Shubhi Agarwal & Ravi Chauhan
Table Of Content
Pre-requisites
- Understanding of open policy agent
- Basic understanding of rego and rego playground
- Opa version : 0.13.5 (minimum)
Introduction to rego
OPA is purpose-built for reasoning around information represented in structured documents. The data that your service and its users publish can be inspected and transformed using OPA’s native query language Rego.
Rego is declarative so policy authors can focus on what queries should return rather than how queries should be executed.
Writing policies in rego can be sometimes tricky mainly because of its declarative nature.
This document compiles some of the important concepts and use-cases that we came across while writing policies. These are quite generic and serves a variety of use-cases. Hopefully, it will benefit a lot of people.
Opa installation steps
- Download opa
1.1. Download using opa binary for your platform from GitHub Releases.
For macOS (64-bit) and Linux (64-bit) :
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_darwin_amd64For Windows download using the link here: Link
1.2. Download and run opa via docker
Docker Link for OPA image: DockerImage
2. Set permissions on the opa executable:
chmod 755 ./opa
3. Run opa as a server
./opa run --server (By default OPA listens for HTTP connections on 0.0.0.0:8181)
4. Curls to push policy and data files, and post a request
Push data file to the server
curl -X PUT http://localhost:8181/v1/data/{myData} --data-binary @{myData}.json
Push rego file to the server
curl -X PUT http://localhost:8181/v1/policies/{mypolicy} --data-binary @{mypolicy}.rego
Execute the rules
curl --location --request POST 'http://localhost:8181/v1/data/$policyPath$/{ruleName}' \
--header 'Content-Type: text/plain' \
--data-raw '{Input_Json}'
For details refer: Run opa as server
5. Run the test on opa
From the root directory containing rego files and data files(JSON), run the following command: opa test . -v
For details refer: OPA Documentation Testing
Some common concepts and use-cases
- What are partial and complete rules?
Rules define the context of the policy document in OPA.
Key in the head can refer to a value, array, object etc. The head of the rule is assigned values that are an aggregation of all the rules that evaluate to true.
- Partial rules
Generating sets: Head declares only keys whose value is defined and returned from the body.
allow[result] {
input.action.id == "HR"
result = "allow HR"
}
allow[result] {
input.action.name == "employee"
result = "allow employee"
}
Playground Link: https://play.openpolicyagent.org/p/nRkaBvzZXw
Generating objects: Head declaring a key and a value for the rule.
allow[result] = action_id{
input.action.id == "HR"
action_id = input.action.id
result = "allow HR"
}
allow[result] = action_id{
input.action.name == "employee"
action_id = input.action.name
result = "allow employee"
}
Playground Link: https://play.openpolicyagent.org/p/C0WIUYMSC2
Rules provide a complete definition by omitting the key in the head. Documents produced by rules with complete definitions can only have one value at a time. If evaluation produces multiple values for the same document, an error will be returned.
Complete rule with no multiple outputsallow = {"result": "allow"}{
input.role =="Employee"
input.action == "salary view"
}
allow = {"result": "deny"} {
input.role =="Manager"
not input.action == "salary view"
}Playground Link: https://play.openpolicyagent.org/p/VnqGE3ZZNsComplete rule with multiple outputsallow = {"result": "allow"}{
input.role =="Employee"
input.action == "salary view"
}
allow = {"result": "deny"} {
input.senior =="Manager"
input.action == "salary view"
}
Output
//eval_conflict_error: complete rules must not produce multiple
outputsPlayground Link: https://play.openpolicyagent.org/p/o2NV002oGo
2. Equality in regos
Rego supports three kinds of equality as mentioned below:
- Assignment(:=)
Assigned variables are locally scoped to that rule and shadow global variables.
employeeType := "HR"
allow {
employeeType := "Manager" # declare local variable
'employeeType' and assign value
Manager
employeeType != "HR" # true because 'employeeType'
refers to local variable
}
Output
{
"allow": true,
"employeeType": "HR"
}
Playground Link: https://play.openpolicyagent.org/p/HkWlDf2HPa
2. Comparison(==)
Comparison checks if two values are equal within a rule. If the left or right-hand side contains a variable that has not been assigned a value, the compiler throws an error.
employee {
input.employeeType == "HR"
}
Playground Link: https://play.openpolicyagent.org/p/sUJ99P7EvX
3. Unification(=)
Unification (=) combines assignment and comparison.
Rego will assign variables to values that make the comparison true. Unification lets you ask for values for variables that make an expression true.
employee = [employeeType,action]{
[action,"Engineer"] = ["viewSalary",employeeType]
}
Output
[
"Engineer",
"viewSalary"
]
Playground Link: https://play.openpolicyagent.org/p/gVSIfFtpKP
3. Iterating arrays
This is useful to verify if an input exists in the array list. Time Complexity of this operation is O(n).
Iterate using [_]
allow {
input.role =="Employee"
allowedActionEmployee :=
["salary_view","update_personal_info","view_personal_info"]
input.action = allowedActionEmployee[_]
}Iterate using an iterator
allow {
input.role =="HR"
allowedActionHR :=
["salary_view","salary_edit","view_personal_info",
"add/remove_employee"]
some i
input.action = allowedActionHR[i]
}
Playground Link: https://play.openpolicyagent.org/p/b8ngVw42Df
2.Iteration through multiple arrays
There are use-cases where we need to compare multiple values corresponding to the value in the static-list. We can use both the iterations above.
We can extract object info corresponding to the same values in two lists along with their index as described below.
#Find the type of all the roles corresponding to the input
role_id_set[input_id] = role_id{
some k,i
role_info.roles[k].id == input.role_set[i]
role_id := role_info.roles[k].type
input_id := input.role_set[i]
}
Playground Link: https://play.openpolicyagent.org/p/Pl9cUbpsfS
Note that, in the above examples, statements that are written below ‘[_]’ or ‘some <variable>’ are always under the loop.
4. Using negations in rego
Rego is existentially quantified. This means that rule bodies and queries express FOR ANY and not FOR ALL. Thus, while using ‘!=’ operator it looks for a single value which is not equal to the value compared, however when we use negations we often need to compare FOR ALL rather than FOR ANY.
allow {
allowed_actions := { "add", "delete", "edit" }
input.action != allowed_actions[_]
}
Input : {"action": "edit"}
Output : {"allow": true }Playground Link: https://play.openpolicyagent.org/p/nvUPWyh3WU
2. Another rule that’s enforced by OPA is that a variable appearing in a negated expression must also appear in another non-negated equality expression in the rule else it will throw an error. For instance,
allow {
allowed_actions :=
{"view_personal_info","edit_personal_info","view_salary",
"update_salary"}
not input.action == data.allowed_actions[_]
}
Output : rego_unsafe_var_error: var _ is unsafePlayground Link: https://play.openpolicyagent.org/p/qtanOZaJdQ
To express FOR ALL in Rego, complement the logic in the ruling body (e.g., ‘!=’ becomes ‘==’) and then, complement the check using negation (e.g. is_Action_Allowed becomes ‘not’ is_Action_Allowed) as shown.
To solve for both the issues, we use negations by using the ‘not’ operator as follows:
allow = true { not is_Action_Allowed }
is_Action_Allowed = true {
allowed_actions := { "add", "delete", "edit" }
input.action == allowed_actions[_]
}
Input : {"action": "edit"}
Output : { "allow": false}Playground Link: https://play.openpolicyagent.org/p/ZL8DU4x2u8
Glob is useful for matching the pattern separated by delimiters as defined. This is suitable for use-cases where regex matching is required or where URL matching helps in defining output.
The default delimiter is [“.”] when delimiter field is empty. Refer to playground link for applications.
api_attributes = {
"[abc]at/dogs" ,
"?et[a-z]/dogs/*",
"pets/dogs/*/adopt",
"pets/dogs/**/adopt"
}
matchUrl {
some path
api_attributes[path]
glob.match(path, ["/"], input.path)
}
Test Case
matchUrl with input as {
"path": "pets/dogs/dog123/dog234/cat123/adopt"
}
Rego Playground: https://play.openpolicyagent.org/p/5QNfjE3hiF
Rego evaluates and returns the output of all the rules that evaluate to true while executing partial rules. This can create conflicts in decision making, especially when both the permit and deny get executed.
These kinds of conflicts can be avoided by wrapping the rules with the parent rule which is complete and maintains the uniqueness of the result.
We solved it by creating an allow rule which is a complete rule and wraps the partial rules to unite to a single decision.
default allow = {"reason": "access denied" }allow = {"permissions": permit } {
permit
not deny
}
allow = { "permissions": deny } {
deny
}permit[x] = y { [x, "hr"] = ["permit", y] }
deny[x] = y { [x, "employee"] = ["deny", y] }
Playground Link: https://play.openpolicyagent.org/p/O63ZYDXani
Debugging in playground/styra is simple but in live environments, it’s challenging to analyse and figure out which rule is executed.
We add a negative rule for each rule we add which will execute when the corresponding positive rule fails to execute.
permit {
is_valid_employee
is_allowed_action_of_employee
has_required_employee_permissions
}
deny[reason] {
not is_valid_employee
reason := "EMPLOYEE_DO_NOT_EXISTS"
}
deny[reason]{
not is_allowed_action_of_employee
reason := "INVALID_ACTION"
}
deny[reason]{
not has_required_employee_permissions
reason := "INVALID_PERMISSIONS"
}
Playground Link: https://play.openpolicyagent.org/p/fKunnjFlbL
8. Hierarchical data traversal
Often we come across use cases where data is static but it branches in various layers like a tree[JSON tree].
For instance: The HTTP request format is hierarchical branching from URI, method type to attribute parameters.
We had one such use case where we needed to find if a mapping exists corresponding to the attribute value in a static data.
Please refer to the playground link to check the exact use-case.
allow {
employeeExistInCompany
employeeIdMap = split(input.employeeId,",")
count(checkMapping(employeeIdMap)) == 0
}
employeeExistInCompany {
count(findMapping)> 0
}
checkMapping(identityProvidersInput) = {a | a := identityProvidersInput[_]} - {b | b := findMapping[_]}findMapping = output {
output := data.ApplicationPartnerMap[input.originalRequestURI][input.httpMethod][input.teamId]
}
Data
{
"ApplicationPartnerMap": {
"/company/hr/manager": {
"GET": {
"team1": [
"Ross",
"Rachel"
],
"team2": [
"Penny"
]
}
},
"/company/tech/employee": {
"GET": {
"team1": [
"Monica",
"Pheobe",
"Chandler"
],
"team2": [
"Lenord"
]
},
"PUT": {
"team3": [
"Mike",
"Gunther",
"Jannice"
]
}
}
}
}
Playground Link: https://play.openpolicyagent.org/p/I2poPkRxX7
9. Traversing environment-specific data
In the software world, we don’t make a release to prod directly instead we have various development environments for quality, performance, end to end testing before we make a release in production.
The data, however, is different in these different environments and there should be some way to identify what to use.
There are various ways we can solve for it.
- Store environment-specific data in separate files and import them
checkMapping(identityProvidersInput) = {a | a := identityProvidersInput[_]} - {b | b := findMapping[_]}findMapping = output {
input.connectingEnvironment == "qal"
output := qal[input.originalRequestURI][input.httpMethod][input.teamId]
}
findMapping = output {
input.connectingEnvironment == "prod"
output := prod[input.originalRequestURI][input.httpMethod][input.teamId]
}
Data
{
"prod": {
...
}
},
"qal": {
"ApplicationPartnerMap": {
...
}
}
}
Playground Link: https://play.openpolicyagent.org/p/dwET4mc19c
2. Maintain single storage for all the environments data described as follows
import data.AllEnvironmentData as appData
findMapping = output {
output := appData[input.connectingEnvironment][input.originalRequestURI][input.httpMethod][input.teamId]
}
Data
{
"AllEnvironmentData": {
"prod": {
...
},
"qal": {
...
}
}
}
Playground Link: https://play.openpolicyagent.org/p/39RW9FUBrv
10. Walking the JSON data hierarchy
Traversing deep down the hierarchy and find out the path exists or not can be solved by using “walk”.
We can manipulate this traversal information in various ways and make deductions.
##find the management chain for role Id in input
foundpath = path {
[path, value] := walk(ManagementChain)
value= input.roleId
}
Data
{
"ManagementChain": {
"HR": 5,
"SVP": {
"Executive Assistant": 1,
"FellowEngineer": {
"DistinguishedEngineer": 2
},
"VP": {
"Manager": 18,
"PrincipalEngineer": 3
}
}
}
}
Playground Link: https://play.openpolicyagent.org/p/nJ9tR0j6VA
We can refactor the raw input received before using it. We also do clean up like remove whitespaces, spellchecks, basic validations, concatenations etc.
But also remember, everything comes at a cost. Therefore, this additional clean up is going to incur some amount of latency and service should be okay with that.
We can then use it to make decisions or return parts of it or the complete object.
#### Request Modification ####
request_preprocessing = preprocessedObject {#### Trim white spaces ####
documentType := trim(input.DocumentType, " ")
action := trim(input.action, " ")
permissionsString := trim(input.Permissions, " ")
#### Check if not null and empty ####
not is_null(permissionsString)
permissionsString != ""
not is_null(documentType)
documentType != ""
not is_null(action)
action != ""
#### concat to for valid action ####
tempArray := array.concat([documentType],[action])
permission := concat(".",tempArray)
preprocessedObject := {"documentType" : documentType,
"action" :action,
"permissionsString" : permissionsString,
"resource-action" : tempArray,
"permission" : permission}
}Playground Link: https://play.openpolicyagent.org/p/12EhSDPu4A
Rego provides great function support.
But sometimes we need to define our utility functions to fulfil the needs of the policy.
Here are examples of the functions that are mostly present in java and replicated in rego.
contains_all_ignore_case(input_list,value_list){
input_list_array := split(lower(input_list),",")
count( {b | b:= lower(value_list[_]) } - {a | a :=
lower(input_list_array[_])}) == 0
}contains_any_ignore_case(input_list,value_list){
input_list_array := split(lower(input_list),",")
diff := {b | b:= lower(value_list[_]) } & {a | a :=
lower(input_list_array[_])}
count(diff) > 0
}
containsIn(PermissionArray, permission) {
PermissionArray[_] = permission
}
Playground Link: https://play.openpolicyagent.org/p/OadLtxjNPX
We often make batch calls in a single request. We can use ‘with’ to iterate over the resources in input and written output as a list. This is how we do it. Please refer to the playground link for a complete example.
batch_authorize = [result |
some i
request := input.requests[i]
result := authorize with input as {
"employeeId" : input.employeeId,
"httpMethod" : request.httpMethod,
"originalRequestURI" : request.originalRequestURI,
"teamId" : request.teamId
}
]
Input
{ ... #common request attributes# ...
"requests" : [ ...]
}Playground Link: https://play.openpolicyagent.org/p/rnvlq55fVA
Rego provides a feature to load static data and use that information to author and derive outcomes from the policy. Based on the given input, how do we search and find a pattern?
An example of it is described below :
##### return all publically accessable apis and method ########
api_attributes = {
"v1/test1": {
"GET": {"readonly": true, "public": true}
...
},
"v2/test2/*": {
...
},
...
}
api_is_read[result]{
some path
value := api_attributes[path]
value[input.method].public == true
result := path
}Input: {"method" : "GET"}Output: [ "v1/test1"]PlayGround Link: https://play.openpolicyagent.org/p/qmkxsHHNQs
Testability
Testing is an important part of the software development process. Rego supports unit testing.
We can write test cases for all the scenarios and check if the system behaves the way we expect it to.
Important features of testing in rego
contains_all_ignore_case(input_list,value_list){
...
}test_contains_all_ignore_case {
contains_all_ignore_case("prep,review",{"prep", "review"})
}
not_test_contains_all_ignore_case {
not contains_all_ignore_case("prep",{"prep", "review"})
}Playground Link: https://play.openpolicyagent.org/p/uydymRpjNY
is_Valid_action{ input.action == data.AllowedAction[_]}test_is_Valid_action {
is_Valid_action with input as {
"action" : "read"
} with data.AllowedAction as MockData
}
test_not_is_Valid_action {
not is_Valid_action with input as {
"action" : "edit"
} with data.AllowedAction as MockData
}Playground Link: https://play.openpolicyagent.org/p/0PAratV6QC
3. Testing rules returning sets/objects
permit[permission]{
...
}test_Permissions {
permit == {"reviewPermission"} with input as {
"role" : "expert"
}
}
test_Permit_developer {
count(getPermission("developer"))>0
with data.permissionList as {
"developer" : "test"
}
}Playground Link : https://play.openpolicyagent.org/p/1QnSa6PfKd
4. Mock data
is_Valid_action{
...
}test_not_is_Valid_action {
not is_Valid_action with input as {
"action" : "edit"
} with data.AllowedAction as MockData
}
MockData = ["read","write","update", "delete"]Playground Link : https://play.openpolicyagent.org/p/cPqybxYqCd
authorize = {"decision": "permit"} {
...
}
authorize = {"decision": "deny"} {
...
}
test_authorize {
authorize.decision =="permit" with input as {
"action": {
"id" : "exp"
}
}
}
Playground Link : https://play.openpolicyagent.org/p/UZe04GBh6J
test_allow {
result := allow with input as {
"action": { "id" : "exp" }
}
result.decision == "permit"
}
test_fail_allow {
result := allow with input as {
"action": { "name" : "adm" }
}
result.decision == "deny"
}
Playground Link: https://play.openpolicyagent.org/p/UyV9hvbr9P
For all the above examples, please find Github repository below:
Github-link: https://github.com/shubhi-8/RegoCheatSheetExamples