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
The condition block that must evaluate to true for the rule to match.
Contains the matching logic. Always required inside condition.
A single CEL expression. Use this for simple conditions.
Logical AND - all expressions must be true. Alternative to expr.
Logical OR - at least one expression must be true. Alternative to expr.
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
Ownership
Status
Geography/Department
Time
Hierarchical
# 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
# Specific status
expr : R.attr.status == "PENDING_APPROVAL"
# Multiple allowed statuses
expr : R.attr.status in ["PENDING", "IN_PROGRESS"]
# Not archived
expr : R.attr.status != "ARCHIVED"
# Same department
expr : R.attr.department == P.attr.department
# Same region
expr : R.attr.region == P.attr.region
# Manager of resource owner
expr : R.attr.owner in P.attr.direct_reports
# Created in last 24 hours
expr : timestamp(R.attr.created_at).timeSince() < duration("24h")
# Business hours only
expr : now().getHours() >= 9 && now().getHours() < 17
# Not expired
expr : timestamp(R.attr.expires_at) > now()
# Weekend
expr : now().getDayOfWeek() >= 5
# Org hierarchy
expr : P.attr.org_id == R.attr.org_id
# Team membership
expr : P.attr.teams.hasIntersection(R.attr.allowed_teams)
# Parent resource ownership
expr : R.attr.parent_id in P.attr.owned_resources
Advanced Examples
Multi-Factor Approval
Time and IP-Based Access
Dynamic Permissions Based on Resource Value
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
Syntax Error: unexpected token
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"
Undefined field or variable
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
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
Use Shorthand Variables
Prefer R.attr.owner over request.resource.attr.owner for readability.
Check Field Existence
Use has() for optional fields: expr : has(R.attr.owner) && R.attr.owner == P.id
Extract Common Patterns
Use exported variables/constants for shared logic across policies.
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)
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