Denied Docs

Rego Basics

Learn the fundamentals of the Rego policy language

Rego is the policy language used by Open Policy Agent (OPA) and Denied Platform. It's a declarative language designed specifically for expressing authorization policies. This tutorial covers the fundamentals you need to write effective policies.

What is Rego?

Rego (pronounced "ray-go") is purpose-built for policy decisions. Unlike general-purpose languages, Rego:

  • Is declarative — describe what should be true, not how to compute it
  • Handles unknown data gracefully — missing fields don't cause errors
  • Returns deterministic results — same input always produces same output
  • Is designed for querying structured data — JSON-like data navigation

Basic Structure

In Denied Platform, you write rule conditions — the platform handles the OPA wrapper structure automatically. A policy is simply the conditions that must be true for the rule to match:

input.subject.properties.role == "admin"
input.action.name == "read"

Each line is a condition. All conditions must be true for the rule to match (implicit AND).

How It Works

When you create a policy in Denied:

  1. You write the conditions (the rule body)
  2. The platform wraps your conditions in the proper OPA structure
  3. Your decision node evaluates requests against these conditions

Rules

Rules define when to allow or deny access. Simply list the conditions that must all be true:

input.subject.properties.role == "admin"

This rule matches when the subject's role property is "admin".

The Input Object

Access fields with dot notation:

input.subject.properties.user_id      # "user-123"
input.subject.properties.role         # "editor"
input.action.name                     # "read"
input.resource.properties.owner       # "user-789"

Data Types

Strings

Strings use double quotes:

input.action.name == "read"
input.subject.properties.name == "Alice"

Numbers

Numbers work as expected:

input.resource.properties.size < 1000
input.subject.properties.age >= 18
input.context.request_count == 5

Booleans

Boolean values:

input.subject.properties.verified == true
input.resource.properties.public == false

Arrays

Arrays are ordered lists:

# Check if value is in array using local binding
allowed_roles := ["admin", "editor"]
input.subject.properties.role == allowed_roles[_]

# Access by index (0-based)
input.subject.properties.permissions[0]

# Iterate over array
some role in input.subject.properties.roles
role == "editor"

Objects

Objects are key-value maps:

# Access nested values
input.resource.properties.metadata.owner

# Check if key exists
input.resource.properties.tags["priority"]

Null and Undefined

Rego handles missing values gracefully:

# This won't error if department is missing
# It will simply not match
input.subject.properties.department == "engineering"

If a field doesn't exist, the comparison evaluates to undefined (not true or false), which means the rule body doesn't match. This is a key feature for handling optional fields.

Rules and Conditions

Basic Rules

A rule with one condition:

input.subject.properties.role == "admin"

Multiple Conditions (AND)

All conditions in a rule must be true (implicit AND):

input.action.name == "read"
input.subject.properties.verified == true
input.resource.properties.public == true

This matches when:

  • The action is "read" AND
  • The subject is verified AND
  • The resource is public

Multiple Rules (OR)

In Denied Platform, you create separate policies for OR logic — if any policy matches, access is allowed:

Policy 1: Admin Access

input.subject.properties.role == "admin"

Policy 2: Owner Access

input.resource.properties.owner == input.subject.properties.user_id

Policy 3: Public Read Access

input.action.name == "read"
input.resource.properties.visibility == "public"

Access is allowed if Policy 1 OR Policy 2 OR Policy 3 matches.

Negation (NOT)

Use not to negate conditions:

not input.resource.properties.restricted

For deny policies:

not input.subject.properties.verified

Comparison Operators

OperatorDescriptionExample
==Equalinput.action.name == "read"
!=Not equalinput.subject.properties.role != "guest"
<Less thaninput.subject.properties.age < 18
>Greater thaninput.subject.properties.level > 5
<=Less than or equalinput.context.count <= 100
>=Greater than or equalinput.subject.properties.score >= 80

Working with Arrays

Membership

Check if a value is in an array using local bindings:

# Define allowed values and check membership
allowed_roles := ["admin", "editor"]
input.subject.properties.role == allowed_roles[_]
# Multiple allowed actions
allowed_actions := ["read", "list", "view"]
input.action.name == allowed_actions[_]

Iteration

Iterate over array elements:

# Check if any role matches
some role in input.subject.properties.roles
role == "editor"
# With index
some i, role in input.subject.properties.roles
role == "admin"

Array Operations

# Count elements
count(input.subject.properties.roles) > 0
# Check all elements match
every role in input.subject.properties.roles {
    role != "banned"
}

Working with Objects

Accessing Values

# Direct access
input.resource.properties.owner

# Nested access
input.resource.properties.metadata.classification

# With bracket notation
input.resource.properties["owner"]
input.resource.properties.tags["priority"]

Checking Keys

# Check if key exists and matches value
input.resource.properties.metadata.classification
input.resource.properties.metadata.classification == "public"

Iterating Objects

# Iterate key-value pairs
some key, value in input.resource.properties.permissions
key == input.subject.properties.user_id
value == "write"

Built-in Functions

Rego provides many built-in functions:

String Functions

# Contains
contains(input.resource.properties.path, "/admin")

# Starts/ends with
startswith(input.resource.properties.path, "/api/")
endswith(input.resource.properties.name, ".pdf")

# Lower/upper case
lower(input.subject.properties.name) == "alice"

# String formatting
sprintf("user-%s", [input.subject.properties.user_id])

Numeric Functions

# Absolute value
abs(input.context.difference)

# Min/max
max([input.resource.properties.a, input.resource.properties.b])
min([1, 2, 3])

# Rounding
round(input.subject.properties.score)
ceil(input.resource.properties.value)
floor(input.resource.properties.amount)

Collection Functions

# Count
count(input.resource.properties.items) > 0

# Sum
sum(input.resource.properties.values)

# Sort
sort(input.resource.properties.items)

# Intersection
intersection({input.resource.properties.required_roles}, {input.subject.properties.roles})

Type Checking

# Check types
is_string(input.subject.properties.name)
is_number(input.subject.properties.age)
is_boolean(input.subject.properties.verified)
is_array(input.subject.properties.roles)
is_object(input.resource.properties.metadata)
is_null(input.resource.properties.optional)

Variables and Assignment

Local Variables

Use := to assign variables:

user_role := input.subject.properties.role
user_role == "admin"

Computed Values

required_level := input.resource.properties.min_level
user_level := input.subject.properties.clearance
user_level >= required_level

Comprehensions

Array Comprehension

Build arrays from iterations:

# Get all admin users from a list
admin_users := [user |
    some user in input.context.users
    user.role == "admin"
]
count(admin_users) > 0

Set Comprehension

Build sets (unique values):

# Get unique departments
departments := {dept |
    some user in input.context.users
    dept := user.department
}
input.subject.properties.department == departments[_]

Object Comprehension

Build objects:

# Map user IDs to roles
user_roles := {user.id: user.role |
    some user in input.context.users
}
user_roles[input.subject.properties.user_id] == "admin"

Helper Rules

For complex logic, you can define helper conditions using local bindings:

# Check if user is admin
is_admin := input.subject.properties.role == "admin"

# Check if user owns resource
is_owner := input.resource.properties.owner == input.subject.properties.user_id

# Use helpers in your conditions
is_admin

Or for owner access with action restrictions:

is_owner := input.resource.properties.owner == input.subject.properties.user_id
allowed_actions := ["read", "update", "delete"]

is_owner
input.action.name == allowed_actions[_]

Common Patterns

Role Check

required_role := "editor"
input.subject.properties.role == required_role

Permission Check

permission := sprintf("%s:%s", [input.resource.properties.type, input.action])
allowed_permissions := input.subject.properties.permissions
permission == allowed_permissions[_]

Ownership Check

input.resource.properties.owner == input.subject.properties.user_id

Property Match

input.subject.properties.department == input.resource.properties.department

Debugging Tips

Use Print Statements

During development, use print to debug:

print("Checking user:", input.subject.properties.user_id)
print("Has role:", input.subject.properties.role)
input.subject.properties.role == "admin"

Remove print statements before deploying to production. They're useful for debugging but add overhead.

Test Incrementally

Build policies step by step:

  1. Start with a simple rule that matches everything
  2. Add one condition at a time
  3. Test after each change in the Playground
  4. Refine until the policy is complete

Handle Missing Fields

Be defensive about optional fields:

# This might not work if department is missing
input.subject.properties.department == "engineering"
# More defensive version using object.get
dept := object.get(input.subject.properties, "department", "")
dept == "engineering"

Next Steps

Now that you understand Rego basics:

On this page