Overview
title: “Write Policies” description: “You may need to learn how to write Rego policies for your application if you aren’t using a policy produced by another team.” lead: “You may need to learn how to write Rego policies for your application if you aren’t using a policy produced by another team.” date: 2020-10-13T15:21:01+02:00 lastmod: 2020-10-13T15:21:01+02:00 draft: false images: [] menu: docs: parent: “how-to” weight: 204 toc: true
Overview
Rego is a declarative language designed for writing policies for your application. It enables software engineers to treat policies as code. Rego provides built-in support for testing and has tools to report code coverage.
Rego v1 features
To opt into Rego v1 language features (new keywords / behavior), import the future.keywords package at the top of your module (after the package line). Minimal example:
package exampleimport future.keywordsdefault allow = falseallow if{ input.user == "admin" }To use Rego v1 features (for example the if keyword), Place this import import future.keywords near the top of any module that requires Rego v1 behavior.
Input query format
Decision API
The input data passed through the POST request body of decision API is fed into the policy engine. In addition, the appliance validates the JWT token extracted from the request’s authorization header, extracts the claims and forwards them to the policy engine.
For example, consider this input data to decision endpoint:
{ "input" : { "method": "GET", "user": "bob", }}The input passed to policy engine will look like:
{ "method": "GET", "user": "bob", "appliance_auth": { "jwt" : "appliance-jwt-token", "claims": { ... } }}These input fields are accessible in rego files through the global input keyword.
In the above example, the claims can be accessed in a rego policy as input.appliance_auth.claims.
Middleware usecase
A middleware can gate an application’s API calls and consult appliance for authorization decisions. In addition to the input data, a middleware will pass the user JWT token used for upstream API calls into the POST request body of decision API. The appliance will validate this token, extract the claims, and forward them to the policy engine. A sample request made by a middleware looks like:
{ "input" : { "method": "GET", "user": "bob", }, "auth": "Bearer sample-jwt-token"}In this case, the input passed to the policy engine will look like:
{ "method": "GET", "user": "bob", "appliance_auth": { "jwt" : "appliance-jwt-token", "claims": { ... } }, "caller_auth": { "jwt": "sample-jwt-token", "claims": { ... } }}In this example, a rego policy can access the user token as input.caller_auth.jwt.
Envoy usecase
Envoy proxy can be setup to gate an application’s API calls. Envoy can be configured to use the appliance as an external authorization filter as explained here. The appliance will validate the user token used to make the upstream API call and extract the claims. The appliance will send the following information to the policy engine:
- The upstream API path
- The HTTP method used
- The user token
- The user claims
This input to the policy engine looks like:
{ "method": "GET", "path": "/files", "caller_auth": { "jwt": "sample-jwt-token", "claims": { ... } }}An example policy
We would like to write policies to query whether a given user or an application has read / write / delete permissions on a file stored in a repository. The policies can be defined as:
- A user with delete permission in a file’s ACL has read, write and delete access.
- A user with write permission in a file’s ACL has read and write access, but not delete access.
- A user with read permission in a file’s ACL has only read access.
We can write these policies in rego as:
package trimble.cloud.fileservice.file
import future.keywords
url_file_id(file_id) = concat("/", ["https://custom-file-service/api/files", file_id])
default can_read := false
# check for read permissions on a file by file idcan_read { resp := trimble.http.get(url_file_id(input.file_id), "") some user, permission in resp.body.file.acl user == input.user_id permission in ["r", "w", "d"]}
default can_write := false
# check for write permissions on a file by file idcan_write { resp := trimble.http.get(url_file_id(input.file_id), "") some user, permission in resp.body.file.acl user == input.user_id permission in ["w", "d"]}
default can_delete := false
# check for delete permissions on a file by file idcan_delete { resp := trimble.http.get(url_file_id(input.file_id), "") some user, permission in resp.body.file.acl user == input.user_id permission in ["d"]}The unit tests for these policies can be written as:
package trimble.cloud.dataocean.file
import future.keywords
mock_http_get("https://custom-file-service/api/files", "") := {"body" : {"file": {"acl" : {"trn:tid:user:459aqz" : "d"}}}}mock_http_get_fail("https://custom-file-service/api/files", "") := {"body" : {"file": {"acl" : {}}}}
test_can_read_fail_by_id if { not can_read with input as {"file_id": "12abc", "user_id": "trn:tid:user:459aqz"} with trimble.http.get as mock_http_get_fail}
test_can_read_by_id if { can_read with input as {"file_id": "12abc", "user_id": "trn:tid:user:459aqz"} with trimble.http.get as mock_http_get}
test_can_write_fail_by_id if { not can_write with input as {"file_id": "12abc", "user_id": "trn:tid:user:459aqz"} with trimble.http.get as mock_http_get_fail}
test_can_write_by_id if { can_write with input as {"file_id": "12abc", "user_id": "trn:tid:user:459aqz"} with trimble.http.get as mock_http_get}
test_can_delete_fail_by_id if { not can_delete with input as {"file_id": "12abc", "user_id": "trn:tid:user:459aqz"} with trimble.http.get as mock_http_get_fail}
test_can_delete_by_id if { can_delete with input as {"file_id": "12abc", "user_id": "trn:tid:user:459aqz"} with trimble.http.get as mock_http_get}Notice how we mocked out the external dependency so that we could test the policies in isolation. This example shows how Rego enables us to treat the policies as testable code.
Multiple Packages - An example
A rego module can declare dependencies on documents defined outside the package. Here is an example:
Folder structure
trimble/├── authz/│ ├── policy.rego│ └── data.rego├── data_access/│ ├── policy.rego│ └── authz.rego└── main.regoRego policies
authz/policy.rego
package trimble.authz
# Default policy to denydefault allow = false
# Allow if user is authenticatedallow { authenticated == true}authz/data.rego
package trimble.authz
# Determine if user is authenticatedauthenticated = true { input.user != ""}data_access/policy.rego
package trimble.data_access
import data.trimble.authz
# Allow read access based on authz policyallow_read { authz.allow}data_access/authz.rego
package trimble.data_access
import input.identity as identityimport data.trimble.authz
# Default policy to denydefault allow = false
# Allow if user is admin and authz policy is allowedallow { identity.role == "admin" authz.allow}main.rego
package trimble.main
# Import policies from different packagesimport data.trimble.data_access as data_accessimport data.trimble.authz as authz_policy
# Evaluate and collect resultsresults = { "data_access_policy": data_access.allow_read, "authz_policy": data_access.allow, "authz_default": authz_policy.allow}Writing Policies for Envoy / middleware
Envoy or middleware is configured to send all the authorization requests to a single pre-defined endpoint such as auth.authz.allow. This maps to a single
Rego rule named allow which evaluates to a true or false. In practice, an organization will have several packages with various rules. As such, the policies
will need to be organzied such that the high level rule can map incoming queries to appropriate rules. This pattern is referred to as
Dynamic Policy Composition and is desribed here.
An example
Here we have two policies to authorize access to the two endpoints supported in our web application: /engineers and /managers.
A top level rule in the main module will route the policy request to the appropriate sub-policy.
Folder structure
trimble/├── policies/| ├── engineers/| │ ├── policy.rego| │| ├── managers/| ├── policy.rego|└── main.regomain.rego
package main
# Map of resource endpoint to the corresponding policy.applicable_policy := { "/engineers": "engineers", "/managers": "managers"}
name := applicable_policy[input.path]
allow { data.policies[name].allow}engineers/policy.rego
package policies.engineers
allow { input.method == "GET"}managers/policy.rego
package policies.managers
allow { input.method == "POST"}
allow { input.method == "GET"}Tips and Tricks
Rule defaults and safety
- Always set defaults for decision rules. Without a default value, a rule can evaluate to
undefinedinstead offalse.
default allow := false- Guard cached data before dereferencing.
trimble.get_cached_datacan returnnullfor cache misses.
cached := trimble.get_cached_data(cache_key)cached != nullcached.user_id == input.user_idRego operators and logic
- Use
:=for assignment and==for comparison.
user := input.user_iduser == "trn:tid:user:123"- Use
infor membership checks.
"admin" in input.rolesnot "blocked" in input.roles- Avoid using
!=in loops to express absence. The following is usually incorrect because it is true when any element is not"blocked":
input.roles[_] != "blocked"Prefer:
not "blocked" in input.roles- Use
everywhen you need all elements to satisfy a condition.
every perm in input.permissions { perm.active == true}- Use
elsefor ordered rule priority when first-match behavior is required.
allow { input.user_type == "admin"} else { input.user_type == "member" input.method == "GET"}Strings and regex
- Use raw backtick strings for regex to avoid double escaping.
regex.match(`^\d{4}-\d{2}-\d{2}$`, input.date)- Prefer string interpolation for user-facing messages when fields may be missing.
msg := $"Access denied for user {input.user_id}"Variables and scoping
- Use
someto declare local iteration variables and avoid accidental variable capture.
some iinput.permissions[i].name == "read"_is a fresh iterator each time it appears.
input.sites[_].servers[_].hostname == input.target_hostPerformance and builtins
- Trimble builtins are non-deterministic and are not memoized. Avoid repeated calls to the same builtin expression inside a rule.
perms := trimble.iam.permissions.get(input.user_id)"files.read" in permsThis pattern is preferred over calling trimble.iam.permissions.get(input.user_id) multiple times in one rule.
Builtins Reference
Trimble-specific builtins
HTTP requests
trimble.http.get(url, token)— GET request with Bearer token authorizationtrimble.http.post(url, body, token)— POST request with Bearer token authorizationtrimble.http.get_with_caller_auth(url)— GET request using caller’s JWTtrimble.http.post_with_caller_auth(url, body)— POST request using caller’s JWT
IAM and roles
trimble.iam.permissions.get(userId, on, scopedToApplicationId, token)— Retrieve user permissions; handles pagination internallytrimble.iam.roles.get(userId, on, scopedToApplicationId, token)— Retrieve user roles; handles pagination internally
Entitlements
trimble.entitlements.licenses.get(env, token, accountId)— Retrieve product licenses from Trimble EMS
Utilities
trimble.get_env(envType)— Get environment identifiers; use"TAM"for actual environment name (“dev”, “stage”, “prod”), or"TID"for placeholdertrimble.get_cached_data(cacheKey)— Retrieve cached data from Redis; returnsnullon cache miss
Standard OPA/Rego builtins
| Builtin | Purpose | Example |
|---|---|---|
some | Iterate over collection members | some p in permissions |
every | Check that all members satisfy a condition | every p in perms { p.active } |
in | Membership test | "admin" in roles |
not | Negation | not allow |
regex.match() | Regex matching | regex.match(^\d{4}-\d{2}-\d{2}$, date) |
string operations | Concatenate, split, format | concat("/", ["api", "users"]) |
:= | Variable assignment | user := input.user_id |
== | Equality comparison | user == "alice" |
!= | Inequality comparison | role != "guest" |
$"..." | String interpolation | msg := $"User {id} denied" |
default | Set rule default value | default allow := false |
Important notes
- All Trimble builtins are non-deterministic and not memoized. Call once and store the result in a variable to avoid redundant external calls.
- HTTP calls only 2xx status codes as success. Non-2xx responses throw an error and halt rule evaluation.
- Cache misses return
null. Always guard:cached := trimble.get_cached_data(key); cached != null; ... - Never use
!=in loops to check absence. Usenot "value" in collectioninstead.