Conditions

This documentation is for an as-yet unreleased version of Cerbos. Choose 0.40.0 from the version picker at the top right or navigate to https://docs.cerbos.dev for the latest version.

A powerful feature of Cerbos policies is the ability to define conditions that are evaluated against the data provided in the request. Conditions are written using the Common Expression Language (CEL).

Cerbos ships with an interactive REPL that can be used to experiment with writing CEL conditions. It can be started by running cerbos repl. See the REPL documentation for more information.

Every condition expression must evaluate to a boolean true/false value. A condition block in a policy can contain either a single condition expression, or multiple expressions combined using the all, any, or none operators. These logical operators may be nested.

Condition block
condition:
  match:
    all:
      of:
        - expr: request.resource.attr.status == "PENDING_APPROVAL"
        - expr: >
            "GB" in request.resource.attr.geographies

Top-level identifiers

Within a condition expression, you have access to several top-level identifiers:

request

Data provided in the check or plan request (principal, resource, and auxiliary data).

runtime

Additional data computed while evaluating the policy.

variables

Variables declared in the variables section of the policy.

constants

Variables declared in the constants section of the policy.

globals

Global variables declared in the policy engine configuration.

There are also single-letter aliases available to allow you to write terser expressions:

P

request.principal

R

request.resource

V

variables

C

constants

G

globals

The request object
request:
  principal: (1)
    id: alice (2)
    roles: (3)
      - employee
    attr: (4)
      geography: GB

  resource: (5)
    kind: leave_request (6)
    id: XX125 (7)
    attr: (8)
      owner: alice

  auxData: (9)
    jwt: (10)
      iss: acme.corp
1 The principal whose permissions are being checked.
2 ID of the principal.
3 Static roles that are assigned to the principal by your identity management system.
4 Free-form context data about the principal.
5 The resource on which the principal is performing actions.
6 Resource kind.
7 ID of the resource instance.
8 Free-form context data about the resource instance.
9 Auxiliary data sources.
10 JWT claims.
The runtime object
runtime:
  effectiveDerivedRoles: (1)
    - owner
    - gb_employee
1 Derived roles that were assigned to to the principal by Cerbos while evaluating the policy. This is only populated in expressions in resource policies, and only includes derived roles that are referenced in at least one policy rule.

Expressions and blocks

Single boolean expression
condition:
  match:
    expr: P.id.matches("^dev_.*")
all operator: all expressions must evaluate to true (logical AND)
condition:
  match:
    all:
      of:
        - expr: R.attr.status == "PENDING_APPROVAL"
        - expr: >
            "GB" in R.attr.geographies
        - expr: P.attr.geography == "GB"
any operator: only one of the expressions has to evaluate to true (logical OR)
condition:
  match:
    any:
      of:
        - expr: R.attr.status == "PENDING_APPROVAL"
        - expr: >
            "GB" in R.attr.geographies
        - expr: P.attr.geography == "GB"
none operator: none of the expressions should evaluate to true (logical negation)
condition:
  match:
    none:
      of:
        - expr: R.attr.status == "PENDING_APPROVAL"
        - expr: >
            "GB" in R.attr.geographies
        - expr: P.attr.geography == "GB"
Nesting operators
condition:
  match:
    all:
      of:
        - expr: R.attr.status == "DRAFT"
        - any:
            of:
              - expr: R.attr.dev == true
              - expr: R.attr.id.matches("^[98][0-9]+")
        - none:
            of:
              - expr: R.attr.qa == true
              - expr: R.attr.canary == true

The above nested block is equivalent to the following:

condition:
  match:
    expr: >
      (R.attr.status == "DRAFT" &&
        (R.attr.dev == true || R.attr.id.matches("^[98][0-9]+")) &&
        !(R.attr.qa == true || R.attr.canary == true))
Quotes in expressions

Single and double quotes have special meanings in YAML. To avoid parsing errors when your expression contains quotes, use the YAML block scalar syntax or wrap the expression in parentheses.

expr: >
  "GB" in R.attr.geographies
expr: ("GB" in R.attr.geographies)

Policy variables

To avoid duplication in condition expressions, you can define variables and constants in policies.

Auxiliary data

If you have auxiliary data sources configured, they can be accessed using request.auxData.

Accessing JWT claims
"cerbie" in request.auxData.jwt.aud && request.auxData.jwt.iss == "cerbos"

Operators

CEL has many builtin functions and operators. The fully up-to-date list can be found at https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions.
Operator Description

!

Logical negation (NOT)

-

Subtraction/numeric negation

!=

Unequals

%

Modulo

&&

Logical AND

||

Logical OR

*

Multiplication

+

Addition/concatenation

/

Division

Less than or equal to

<

Less than

==

Equals

>=

Greater than or equal to

>

Greater than

in

Membership in lists or maps

? :

Ternary condition (if-then-else)

Durations

Duration values must be specified in one of the following units. Larger units like days, weeks or years are not supported because of ambiguity around their meaning due to factors such as daylight saving time transitions.

Suffix Unit

ns

Nanoseconds

us

Microseconds

ms

Milliseconds

s

Seconds

m

Minutes

h

Hours

Test data
...
"resource": {
  "kind": "leave_request",
  "attr": {
    "cooldownPeriod": "3750s",
    "lastAccessed": "2021-04-20T10:00:20.021-05:00"
  }
}
...
Function Description Example

duration

Convert a string to a duration. The string must contain a valid duration suffixed by one of ns, us, ms, s, m or h. E.g. 3750s

duration(R.attr.cooldownPeriod).getSeconds() == 3750

getHours

Get hours from a duration

duration(R.attr.cooldownPeriod).getHours() == 1

getMilliseconds

Get milliseconds from a duration

duration(R.attr.cooldownPeriod).getMilliseconds() == 3750000

getMinutes

Get minutes from a duration

duration(R.attr.cooldownPeriod).getMinutes() == 62

getSeconds

Get seconds from a duration

duration(R.attr.cooldownPeriod).getSeconds() == 3750

timeSince

Time elapsed since the given timestamp to current time on the server. This is a Cerbos extension to CEL

timestamp(R.attr.lastAccessed).timeSince() > duration("1h")

Hierarchies

The hierarchy functions are Cerbos-specific extensions to CEL.
Test data
...
"principal": {
  "id": "john",
  "roles": ["employee"],
  "attr": {
    "scope": "foo.bar.baz.qux",
  }
},
"resource": {
  "kind": "leave_request",
  "attr": {
    "scope": "foo.bar",
  }
}
...
Function Description Example

hierarchy

Convert a dotted string or a string list to a hierarchy

hierarchy("a.b.c") == hierarchy(["a","b","c"])

hierarchy

Convert a delimited string representation to a hierarchy

hierarchy("a:b:c", ":").size() == 3

ancestorOf

Returns true if the first hierarchy shares a common prefix with the second hierarchy

hierarchy("a.b").ancestorOf(hierarchy("a.b.c.d")) == true

commonAncestors

Returns the common ancestor hierarchy

hierarchy(R.attr.scope).commonAncestors(hierarchy(P.attr.scope)) == hierarchy("foo.bar")

descendentOf

Mirror function of ancestorOf

hierarchy("a.b.c.d").descendentOf(hierarchy("a.b")) == true

immediateChildOf

Returns true if the first hierarchy is a first-level child of the second hierarchy

hierarchy("a.b.c").immediateChildOf(hierarchy("a.b")) == true && hierarchy("a.b.c.d").immediateChildOf(hierarchy("a.b")) == false

immediateParentOf

Mirror function of immediateChildOf

hierarchy("a.b").immediateParentOf(hierarchy("a.b.c")) == true && hierarchy("a.b").immediateParentOf(hierarchy("a.b.c.d")) == false

overlaps

Returns true if one of the hierarchies is a prefix of the other

hierarchy("a.b.c").overlaps(hierarchy("a.b.c.d.e")) == true && hierarchy("a.b.x").overlaps(hierarchy("a.b.c.d.e")) == false

siblingOf

Returns true if both hierarchies share the same parent

hierarchy("a.b.c").siblingOf(hierarchy("a.b.d")) == true

size

Returns the number of levels in the hierarchy

hierarchy("a.b.c").size() == 3

[]

Access a level in the hierarchy

hierarchy("a.b.c.d")[1] == "b"

IP addresses

The IP address functions are Cerbos-specific extensions to CEL.
Test data
...
"principal": {
  "id": "elmer_fudd",
  "attr": {
    "ipv4Address": "192.168.0.10",
    "ipv6Address": "2001:0db8:0000:0000:0000:0000:1000:0000"
  }
}
...
Function Description Example

inIPAddrRange

Check whether the IP address is in the range defined by the CIDR

P.attr.ipv4Address.inIPAddrRange("192.168.0.0/24") && P.attr.ipv6Address.inIPAddrRange("2001:db8::/48")

Lists and maps

Test data
...
"principal": {
  "id": "elmer_fudd",
  "attr": {
    "id": "125",
    "teams": ["design", "communications", "product", "commercial"],
    "limits": {
        "design": 10,
        "product": 25
    },
    "clients": {
      "acme": {"active": true},
      "bb inc": {"active": true}
    }
  }
}
...
Operator/Function Description Example

+

Concatenates lists

P.attr.teams + ["design", "engineering"]

[]

Index into a list or a map

P.attr.teams[0] == "design" && P.attr.clients["acme"]["active"] == true

all

Check whether all elements in a list match the predicate.

P.attr.teams.all(t, size(t) > 3) && [1, 2, 3].all(i, j, i < j)

distinct

Returns the distinct elements of a list

[1, 2, 2, 3, 3, 3].distinct() == [1, 2, 3]

except

Produces the set difference of two lists

P.attr.teams.except(["design", "engineering"]) == ["communications", "product", "commercial"]

exists

Check whether at least one element matching the predicate exists in a list or map.

P.attr.teams.exists(t, t.startsWith("comm")) && P.attr.limits.exists(k, v, k == "design" && v > 0)

exists_one

Check that only one element matching the predicate exists.

P.attr.teams.exists_one(t, t.startsWith("comm")) == false && P.attr.limits.exists_one(k, v, k == "design" && v > 0) == false

filter

Filter a list using the predicate.

size(P.attr.teams.filter(t, t.matches("^comm"))) == 2

flatten

Flattens a list. If an optional depth is provided, the list is flattened to the specified level

[1,2,[],[],[3,4]].flatten() == [1, 2, 3, 4] && [1,[2,[3,[4]]]].flatten(2) == [1, 2, 3, [4]]

hasIntersection

Checks whether the lists have at least one common element

hasIntersection(["design", "engineering"], P.attr.teams)

in

Check whether the given element is contained in the list or map

("design" in P.attr.teams) && ("acme" in P.attr.clients)

intersect

Produces the set intersection of two lists

intersect(["design", "engineering"], P.attr.teams) == ["design"]

isSubset

Checks whether the list is a subset of another list

["design", "engineering"].isSubset(P.attr.teams) == false

lists.range

Returns a list of integers from 0 to n-1

lists.range(5) == [0, 1, 2, 3, 4]

map

Transform each element in a list

"DESIGN" in P.attr.teams.map(t, t.upperAscii())

reverse

Returns the elements of a list in reverse order

[5, 3, 1, 2].reverse() == [2, 1, 3, 5]

size

Number of elements in a list or map

size(P.attr.teams) == 4 && size(P.attr.clients) == 2

slice

Returns a new sub-list using the indexes provided

[1,2,3,4].slice(1, 3) == [2, 3]

sort

Sorts a list with comparable elements

[3, 2, 1].sort() == [1, 2, 3]

sortBy

Sorts a list by a key value, i.e., the order is determined by the result of an expression applied to each element of the list

[{ "name": "foo", "score": 0 },{ "name": "bar", "score": -10 },{ "name": "baz", "score": 1000 }].sortBy(e, e.score).map(e, e.name) == ["bar", "foo", "baz"]

transformList

Converts a map or a list into a list value. The output expression determines the contents of the output list. Elements in the list may optionally be filtered

[1, 2, 3].transformList(i, v, i > 0, 2 * v) == [4, 6] &&
[1, 2, 3].transformList(i, v, 2 * v) == [2, 4, 6]

transformMap

Converts a map or a list into a map value. The key remains unchanged and only the value is changed.

[1, 2, 3].transformMap(i, v, i > 0, 2 * v) == {1: 4, 2: 6}

transformMapEntry

Converts a map or a list into a map value; however, this transform expects the entry expression be a map literal. Elements in the map may optionally be filtered

{'greeting': 'hello'}.transformMapEntry(k, v, {v: k}) == {'hello': 'greeting'}

Math

Function Description Example

math.abs

Returns the absolute value of the numeric type provided as input

math.abs(1.2) == 1.2 && math.abs(-2) == 2

math.bitAnd

Performs a bitwise-AND operation over two int or uint values

math.bitAnd(3u, 2u) == 2u && math.bitAnd(3, 5) == 1 && math.bitAnd(-3, -5) == -7

math.bitNot

Function which accepts a single int or uint and performs a bitwise-NOT ones-complement of the given binary value

math.bitNot(1) == -1 && math.bitNot(-1) == 0 && math.bitNot(0u) == 18446744073709551615u

math.bitOr

Performs a bitwise-OR operation over two int or uint values

math.bitOr(1u, 2u) == 3u && math.bitOr(-2, -4) == -2

math.bitShiftLeft

Perform a left shift of bits on the first parameter, by the amount of bits specified in the second parameter. The first parameter is either a uint or an int. The second parameter must be an int

math.bitShiftLeft(1, 2) == 4 && math.bitShiftLeft(-1, 2) == -4 && math.bitShiftLeft(1u, 2) == 4u && math.bitShiftLeft(1u, 200) == 0u

math.bitShiftRight

Perform a right shift of bits on the first parameter, by the amount of bits specified in the second parameter. The first parameter is either a uint or an int. The second parameter must be an int

math.bitShiftRight(1024, 2) == 256 && math.bitShiftRight(1024u, 2) == 256u && math.bitShiftLeft(1024u, 64) == 0u

math.bitXor

Performs a bitwise-XOR operation over two int or uint values

math.bitXor(3u, 5u) == 6u && math.bitXor(1, 3) == 2

math.ceil

Compute the ceiling of a double value

math.ceil(1.2) == 2.0 && math.ceil(-1.2) == -1.0

math.floor

Compute the floor of a double value

math.floor(1.2) == 1.0 && math.floor(-1.2) == -2.0

math.greatest

Get the greatest valued number present in the arguments

math.greatest([1, 3, 5]) == 5 && math.greatest(1, 3, 5) == 5

math.isFinite

Returns true if the value is a finite number

!math.isFinite(0.0/0.0) && math.isFinite(1.2)

math.isInf

Returns true if the input double value is -Inf or +Inf

math.isInf(1.0/0.0) && !math.isInf(1.2)

math.isNaN

Returns true if the input double value is NaN, false otherwise

math.isNaN(0.0/0.0) && !math.isNaN(1.2)

math.least

Get the least valued number present in the arguments

math.least([1, 3, 5]) == 1 && math.least(1, 3, 5) == 1

math.round

Rounds the double value to the nearest whole number with ties rounding away from zero, e.g. 1.5 → 2.0, -1.5 → -2.0

math.round(1.2) == 1.0 && math.round(1.5) == 2.0 && math.round(-1.5) == -2.0

math.sign

Returns the sign of the numeric type, either -1, 0, 1

math.sign(1.2) == 1.0 && math.sign(-2) == -1 && math.sign(0) == 0

math.trunc

Truncates the fractional portion of the double value

math.trunc(1.2) == 1.0 && math.trunc(-1.2) == -1.0

Strings

Test data
...
"resource": {
  "kind": "leave_request",
  "attr": {
    "id": "125",
    "department": "marketing"
  }
}
...
Function Description Example

base64.encode

Encode as base64

base64.encode(bytes("hello")) == "aGVsbG8="

base64.decode

Decode base64

base64.decode("aGVsbG8=") == bytes("hello")

charAt

Get the character at given index

R.attr.department.charAt(1) == 'a'

contains

Check whether a string contains the given substring

R.attr.department.contains("arket")

endsWith

Check whether a string has the given suffix

R.attr.department.endsWith("ing")

format

Format a string with the given arguments

"department_%s_%d".format(["marketing", 1])

indexOf

Index of the first occurrence of the given character

R.attr.department.indexOf('a') == 1

lastIndexOf

Index of the last occurrence of the given character

R.attr.department.lastIndexOf('g') == 8

lowerAscii

Convert ASCII characters to lowercase

"MARKETING".lowerAscii() == R.attr.department

matches

Check whether a string matches a RE2 regular expression

R.attr.department.matches("^[mM].*g$")

replace

Replace all occurrences of a substring

R.attr.department.replace("market", "engineer") == "engineering"

replace

Replace with limits. Limit 0 replaces nothing, -1 replaces all.

"engineering".replace("e", "a", 1) == "angineering" && "engineering".replace("e", "a", -1) == "anginaaring"

size

Get the length of the string

size(R.attr.department) == 9

split

Split a string using a delimiter

"a,b,c,d".split(",")[1] == "b"

split

Split a string with limits. Limit 0 returns an empty list, 1 returns a list containing the original string.

"a,b,c,d".split(",", 2)[1] == "b,c,d"

startsWith

Check whether a string has the given prefix

R.attr.department.startsWith("mark")

substring

Selects a substring from the string

R.attr.department.substring(4) == "eting" && R.attr.department.substring(4, 6) == "et"

trim

Remove whitespace from beginning and end

" marketing ".trim() == "marketing"

upperAscii

Convert ASCII characters to uppercase

R.attr.department.upperAscii() == "MARKETING"

Timestamps

Test data
...
"resource": {
  "kind": "leave_request",
  "attr": {
    "lastAccessed": "2021-04-20T10:00:20.021-05:00",
    "lastUpdateTime": "2021-05-01T13:34:12.024Z",
  }
}
...
Function Description Example

timestamp

Convert an RFC3339 formatted string to a timestamp

timestamp(R.attr.lastAccessed).getFullYear() == 2021

getDate

Get day of month from a timestamp

timestamp(R.attr.lastAccessed).getDate() == 20

getDayOfMonth

Get day of month from a timestamp. Returns a zero-based value

timestamp(R.attr.lastAccessed).getDayOfMonth() == 19

getDayOfWeek

Get day of week from a timestamp. Returns a zero-based value where Sunday is 0

timestamp(R.attr.lastAccessed).getDayOfWeek() == 2

getDayOfYear

Get day of year from a timestamp. Returns a zero-based value

timestamp(R.attr.lastAccessed).getDayOfYear() == 109

getFullYear

Get full year from a timestamp

timestamp(R.attr.lastAccessed).getFullYear() == 2021

getHours

Get hours from a timestamp

timestamp(R.attr.lastAccessed).getHours() == 10

getMilliseconds

Get milliseconds from a timestamp

timestamp(R.attr.lastAccessed).getMilliseconds() == 21

getMinutes

Get minutes from a timestamp

timestamp(R.attr.lastAccessed).getMinutes() == 5

getMonth

Get month from a timestamp. Returns a zero-based value where January is 0

timestamp(R.attr.lastAccessed).getMonth() == 3

getSeconds

Get seconds from a timestamp

timestamp(R.attr.lastAccessed).getSeconds() == 20

now

Current time on the server. This is a Cerbos extension to CEL

now() > timestamp(R.attr.lastAccessed)

timeSince

Time elapsed since the given timestamp to current time on the server. This is a Cerbos extension to CEL

timestamp(R.attr.lastAccessed).timeSince() > duration("1h")

Example: Assert that more than 36 hours has elapsed between last access time and last update time
timestamp(R.attr.lastUpdateTime) - timestamp(R.attr.lastAccessed) > duration("36h")
Example: Add a duration to a timestamp
timestamp(R.attr.lastUpdateTime) + duration("24h") == timestamp("2021-05-02T13:34:12.024Z")