Skip to content

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 example
import future.keywords
default allow = false
allow 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 id
can_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 id
can_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 id
can_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.rego

Rego policies

authz/policy.rego

package trimble.authz
# Default policy to deny
default allow = false
# Allow if user is authenticated
allow {
authenticated == true
}

authz/data.rego

package trimble.authz
# Determine if user is authenticated
authenticated = true {
input.user != ""
}

data_access/policy.rego

package trimble.data_access
import data.trimble.authz
# Allow read access based on authz policy
allow_read {
authz.allow
}

data_access/authz.rego

package trimble.data_access
import input.identity as identity
import data.trimble.authz
# Default policy to deny
default allow = false
# Allow if user is admin and authz policy is allowed
allow {
identity.role == "admin"
authz.allow
}

main.rego

package trimble.main
# Import policies from different packages
import data.trimble.data_access as data_access
import data.trimble.authz as authz_policy
# Evaluate and collect results
results = {
"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.rego

main.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 undefined instead of false.
default allow := false
  • Guard cached data before dereferencing. trimble.get_cached_data can return null for cache misses.
cached := trimble.get_cached_data(cache_key)
cached != null
cached.user_id == input.user_id

Rego operators and logic

  • Use := for assignment and == for comparison.
user := input.user_id
user == "trn:tid:user:123"
  • Use in for membership checks.
"admin" in input.roles
not "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 every when you need all elements to satisfy a condition.
every perm in input.permissions {
perm.active == true
}
  • Use else for 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 some to declare local iteration variables and avoid accidental variable capture.
some i
input.permissions[i].name == "read"
  • _ is a fresh iterator each time it appears.
input.sites[_].servers[_].hostname == input.target_host

Performance 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 perms

This 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 authorization
  • trimble.http.post(url, body, token) — POST request with Bearer token authorization
  • trimble.http.get_with_caller_auth(url) — GET request using caller’s JWT
  • trimble.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 internally
  • trimble.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 placeholder
  • trimble.get_cached_data(cacheKey) — Retrieve cached data from Redis; returns null on cache miss

Standard OPA/Rego builtins

BuiltinPurposeExample
someIterate over collection memberssome p in permissions
everyCheck that all members satisfy a conditionevery p in perms { p.active }
inMembership test"admin" in roles
notNegationnot allow
regex.match()Regex matchingregex.match(^\d{4}-\d{2}-\d{2}$, date)
string operationsConcatenate, split, formatconcat("/", ["api", "users"])
:=Variable assignmentuser := input.user_id
==Equality comparisonuser == "alice"
!=Inequality comparisonrole != "guest"
$"..."String interpolationmsg := $"User {id} denied"
defaultSet rule default valuedefault 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. Use not "value" in collection instead.