Skip to main content

Overview

Conditions enable attribute-based access control (ABAC) in Cerbos policies. Using the Common Expression Language (CEL), you can write dynamic rules that evaluate request attributes, resource properties, and principal information at runtime. Conditions let you:
  • Check ownership: request.resource.attr.owner == request.principal.id
  • Validate status: request.resource.attr.status == "PENDING_APPROVAL"
  • Enforce geography: request.resource.attr.region == request.principal.attr.region
  • Check time windows: now().getHours() >= 9 && now().getHours() < 17
  • Validate IP ranges: request.principal.attr.ip.inIPAddrRange("10.0.0.0/8")

Basic Syntax

Conditions use CEL (Common Expression Language), a simple, fast expression language.

Simple Condition

rules:
  - actions: ["edit"]
    effect: EFFECT_ALLOW
    roles: ["user"]
    condition:
      match:
        expr: request.resource.attr.owner == request.principal.id

Condition Structure

condition
object
The condition block that must evaluate to true for the rule to match.
match
object
required
Contains the matching logic. Always required inside condition.
expr
string
A single CEL expression. Use this for simple conditions.
all
object
Logical AND - all expressions must be true. Alternative to expr.
any
object
Logical OR - at least one expression must be true. Alternative to expr.
none
object
Logical NOR - no expressions can be true. Alternative to expr.

Request Variables

Cerbos provides several built-in variables for accessing request data:

request.principal

Information about the user or service making the request:
expr: request.principal.id == "alice"                 # Principal ID
expr: "admin" in request.principal.roles              # Check role
expr: request.principal.attr.department == "engineering"  # Custom attribute
Available fields:
  • request.principal.id - Principal identifier (string)
  • request.principal.roles - List of static roles (list of strings)
  • request.principal.attr - Custom attributes (map)
  • request.principal.scope - Request scope (string)

request.resource

Information about the resource being accessed:
expr: request.resource.id == "doc123"                 # Resource ID
expr: request.resource.kind == "document"             # Resource type
expr: request.resource.attr.status == "published"     # Custom attribute
Available fields:
  • request.resource.id - Resource identifier (string)
  • request.resource.kind - Resource type (string)
  • request.resource.attr - Custom attributes (map)
  • request.resource.scope - Resource scope (string)

request.aux_data

Additional request metadata:
expr: request.aux_data.jwt.aud == "my-app"            # JWT claims
expr: request.aux_data.ip_address.inIPAddrRange("10.0.0.0/8")  # IP checks
Common fields:
  • request.aux_data.jwt - Decoded JWT claims (if provided)
  • request.aux_data.* - Any custom data passed in the request

Shorthand Variables

For cleaner expressions, use these shortcuts:
  • P = request.principal
  • R = request.resource
  • V = variables (see Variables section)
  • C = constants (see Constants section)
# Verbose
expr: request.resource.attr.owner == request.principal.id

# Shorthand
expr: R.attr.owner == P.id

Logical Operators

Single Expression (expr)

For simple conditions:
condition:
  match:
    expr: R.attr.owner == P.id

All (AND)

All conditions must be true:
condition:
  match:
    all:
      of:
        - expr: R.attr.status == "PENDING"
        - expr: R.attr.geography == P.attr.geography
        - expr: R.attr.amount < 10000

Any (OR)

At least one condition must be true:
condition:
  match:
    any:
      of:
        - expr: R.attr.public == true
        - expr: R.attr.owner == P.id
        - expr: "admin" in P.roles

None (NOR)

No conditions can be true:
condition:
  match:
    none:
      of:
        - expr: R.attr.archived == true
        - expr: R.attr.deleted == true

Nested Logic

Combine logical operators:
condition:
  match:
    all:
      of:
        - expr: R.attr.status == "active"
        - any:  # Nested OR within AND
            of:
              - expr: R.attr.owner == P.id
              - expr: "manager" in P.roles

CEL Operators

Comparison

expr: R.attr.age == 25          # Equal
expr: R.attr.age != 25          # Not equal
expr: R.attr.age > 18           # Greater than
expr: R.attr.age >= 18          # Greater than or equal
expr: R.attr.age < 65           # Less than
expr: R.attr.age <= 65          # Less than or equal

Logical

expr: R.attr.active && R.attr.verified        # AND
expr: R.attr.public || R.attr.owner == P.id   # OR
expr: !R.attr.archived                         # NOT

String Operations

expr: R.attr.name.startsWith("Project_")           # Starts with
expr: R.attr.email.endsWith("@example.com")        # Ends with
expr: R.attr.description.contains("urgent")        # Contains substring
expr: R.attr.name.matches("^[A-Z][a-z]+$")        # Regex match
expr: R.attr.status in ["PENDING", "APPROVED"]    # In list

Arithmetic

expr: R.attr.price * 1.1 > 100                     # Multiplication
expr: R.attr.total - R.attr.discount < 50          # Subtraction
expr: R.attr.quantity + 5 <= P.attr.quota          # Addition
expr: R.attr.score / R.attr.attempts > 0.8         # Division

List Operations

expr: "admin" in P.roles                           # Membership
expr: P.id in R.attr.collaborators                 # Check if in list
expr: R.attr.tags.size() > 0                       # List size
expr: R.attr.items.exists(i, i.status == "active") # Any match (exists)
expr: R.attr.items.all(i, i.verified == true)      # All match

Cerbos-Specific Functions

Cerbos extends CEL with custom functions for common authorization patterns:

Set Operations

Check if two lists share any elements:
expr: P.attr.teams.hasIntersection(R.attr.allowed_teams)
expr: ["read", "write"].hasIntersection(P.attr.permissions)
Check if one list is a subset of another:
expr: R.attr.required_roles.isSubset(P.roles)
expr: ["read"].isSubset(["read", "write", "admin"])
Get the intersection of two lists:
expr: P.attr.teams.intersect(R.attr.teams).size() > 0
Get elements in first list but not in second:
expr: R.attr.required_perms.except(P.attr.permissions).size() == 0

Time Functions

Get current timestamp:
expr: now().getHours() >= 9 && now().getHours() < 17  # Business hours
expr: now().getDayOfWeek() < 6                        # Weekday (Mon=0)
expr: now() < timestamp("2024-12-31T23:59:59Z")      # Before date
Parse timestamp from string:
expr: timestamp(R.attr.expires_at) > now()
expr: timestamp(R.attr.created_at).getFullYear() == 2024
Duration since a timestamp:
expr: timestamp(R.attr.created_at).timeSince() < duration("24h")
expr: timestamp(R.attr.modified_at).timeSince() > duration("7d")
Parse duration from string:
expr: duration("1h")   # 1 hour
expr: duration("30m")  # 30 minutes
expr: duration("7d")   # 7 days

Network Functions

Check if IP address is in CIDR range:
expr: P.attr.ip_address.inIPAddrRange("10.0.0.0/8")     # Private network
expr: P.attr.ip_address.inIPAddrRange("192.168.1.0/24") # Subnet

Type Checking

Check if field exists:
expr: has(R.attr.owner) && R.attr.owner == P.id
expr: has(P.attr.department)  # Check attribute exists
Get value type:
expr: type(R.attr.value) == string
expr: type(R.attr.count) == int

Variables

Define reusable expressions:

Top-Level Variables

---
apiVersion: api.cerbos.dev/v1
variables:
  is_owner: R.attr.owner == P.id
  is_public: R.attr.public == true
  is_recent: |-
    timestamp(R.attr.created_at).timeSince() < duration("24h")
  pending_status: '"PENDING_APPROVAL"'  # String literal

resourcePolicy:
  resource: document
  version: "default"
  rules:
    - actions: ["edit"]
      effect: EFFECT_ALLOW
      roles: ["user"]
      condition:
        match:
          any:
            of:
              - expr: V.is_owner
              - expr: V.is_public && V.is_recent

Local Variables (in policies)

resourcePolicy:
  resource: document
  version: "default"
  variables:
    local:
      same_dept: R.attr.department == P.attr.department
      is_manager: '"manager" in P.roles'
    import:
      - common_variables  # From export_variables/common_variables.yaml
  rules:
    - actions: ["approve"]
      condition:
        match:
          all:
            of:
              - expr: V.same_dept
              - expr: V.is_manager

Exported Variables

Share variables across policies:
# exports/common_variables.yaml
---
apiVersion: api.cerbos.dev/v1
exportVariables:
  name: common_variables
  definitions:
    is_owner: R.attr.owner == P.id
    is_public: R.attr.public == true
    business_hours: now().getHours() >= 9 && now().getHours() < 17
Import and use:
resourcePolicy:
  resource: document
  variables:
    import:
      - common_variables
  rules:
    - actions: ["edit"]
      condition:
        match:
          expr: V.is_owner && V.business_hours

Constants

Define static values:

Local Constants

resourcePolicy:
  resource: leave_request
  version: "default"
  constants:
    local:
      max_days: 30
      approval_threshold: 10000
      pending_status: "PENDING_APPROVAL"
  rules:
    - actions: ["approve"]
      condition:
        match:
          all:
            of:
              - expr: R.attr.days <= C.max_days
              - expr: R.attr.status == C.pending_status

Exported Constants

# exports/app_constants.yaml
---
apiVersion: api.cerbos.dev/v1
exportConstants:
  name: app_constants
  definitions:
    max_file_size: 104857600  # 100MB
    allowed_regions: ["us-east-1", "us-west-2", "eu-west-1"]
    admin_email_domain: "@admin.example.com"
Import and use:
resourcePolicy:
  resource: document
  constants:
    import:
      - app_constants
  rules:
    - actions: ["upload"]
      condition:
        match:
          expr: R.attr.size <= C.max_file_size

Common Patterns

# User owns resource
expr: R.attr.owner == P.id

# User in collaborators list
expr: P.id in R.attr.collaborators

# User created resource
expr: R.attr.created_by == P.id

Advanced Examples

rules:
  - actions: ["approve"]
    effect: EFFECT_ALLOW
    roles: ["manager"]
    condition:
      match:
        all:
          of:
            # Must be pending
            - expr: R.attr.status == "PENDING_APPROVAL"
            # Amount within manager's limit OR has senior approval
            - any:
                of:
                  - expr: R.attr.amount < P.attr.approval_limit
                  - expr: R.attr.senior_approved == true
            # Manager's department OR assigned reviewer
            - any:
                of:
                  - expr: R.attr.department == P.attr.department
                  - expr: P.id in R.attr.assigned_reviewers

Debugging Conditions

Common Errors

Check for:
  • Missing quotes around strings: "PENDING" not PENDING
  • Mismatched parentheses or brackets
  • Using = instead of == for comparison
# Wrong
expr: R.attr.status = PENDING

# Correct
expr: R.attr.status == "PENDING"
Verify:
  • Attribute paths match request data structure
  • Variables are defined before use
  • Correct prefix (V. for variables, C. for constants)
# If error: "undefined field 'owner'"
# Check request data:
# - R.attr.owner (correct)
# - R.owner (wrong - owner is in attr)
Ensure types match:
# Wrong: comparing string to int
expr: R.attr.count == "5"

# Correct
expr: R.attr.count == 5

# Use type conversions if needed
expr: int(R.attr.count_str) == 5

Testing Expressions

Use cerbos compile to validate:
cerbos compile /path/to/policies
Errors will show the exact expression and line number:
ERROR [policy.yaml:15] Invalid expression: 
  R.attr.owner = P.id
                ^
Syntax error: unexpected token '='

Best Practices

1

Keep Expressions Simple

Break complex logic into variables:
# Instead of:
expr: R.attr.owner == P.id || ("manager" in P.roles && R.attr.department == P.attr.department)

# Use:
variables:
  is_owner: R.attr.owner == P.id
  is_dept_manager: '"manager" in P.roles && R.attr.department == P.attr.department'

expr: V.is_owner || V.is_dept_manager
2

Use Shorthand Variables

Prefer R.attr.owner over request.resource.attr.owner for readability.
3

Check Field Existence

Use has() for optional fields:
expr: has(R.attr.owner) && R.attr.owner == P.id
4

Extract Common Patterns

Use exported variables/constants for shared logic across policies.
5

Document Complex Logic

Add comments in multi-line expressions:
expr: |-
  // Check if user can approve based on amount and role
  (R.attr.amount < 1000 && "manager" in P.roles) ||
  (R.attr.amount < 10000 && "senior_manager" in P.roles) ||
  ("director" in P.roles)

Performance Tips

  • Avoid expensive operations in conditions: Complex regex, large list iterations
  • Order matters in all and any: Put cheaper checks first
  • Use constants: Cached at compile time, faster than variables
  • Minimize nested checks: Flatten when possible
# Slower: Check expensive condition first
all:
  of:
    - expr: R.attr.content.matches("^[A-Z][a-z]{100,}$")  # Expensive regex
    - expr: R.attr.owner == P.id  # Cheap check

# Faster: Cheap check first (fail fast)
all:
  of:
    - expr: R.attr.owner == P.id  # Cheap check first
    - expr: R.attr.content.matches("^[A-Z][a-z]{100,}$")  # Only if needed

CEL Reference

For complete CEL syntax, see:

Next Steps

Resource Policies

Apply conditions in resource policies

Derived Roles

Use conditions for dynamic roles

Testing

Test your conditions