Skip to main content

Rego Keyword Examples: not

The not keyword is the primary means of expressing negation in Rego. Similar to other keywords in Rego, it can also make your policies more 'English-like' and thus easier to read.

allow if {
not input.user.external
}

Examples

Checking for undefined

One of the most important use cases for not is checking for undefined values. In this example a policy uses not to deny any request without an email set. Even if a value is not used in the policy, it might be important information for the decision log.

Try updating the example input.json, changing e_mail to email. When e_mail is set, then email is undefined and not checks for that in the first rule.

policy.rego
package play

deny contains "missing email" if not input.email

deny contains "under 18" if input.age < 18
Output
{
  "deny": [
    "missing email"
  ]
}
Loading...
input.json
{
"e_mail": "oops@example.com",
"age": 20
}
data.json
{}

Open in OPA Playground

Negation with not

The not keyword is also useful for all kinds of negations. Use negations when you want to required the opposite of a statement.

policy.rego
package play

import future.keywords.not

deny contains "must be staff" if {
not "staff" in input.roles
}

deny contains "must be example.com account" if {
not endswith(input.email, "@example.com")
}

deny contains "cannot be accesed over VPN" if {
not input.is_vpn
}
Output
{
  "deny": [
    "cannot be accesed over VPN",
    "must be example.com account",
    "must be staff"
  ]
}
Loading...
input.json
{
"roles": [
"intern"
],
"email": "alice@foo.example.com",
"via_vpn": true
}
data.json
{}

Open in OPA Playground

Improved Negation Semantics

The future.keywords.not import fixes a long-standing semantic issue with negation in Rego.

The problem with legacy negation

Without the import, the compiler expands a negated composite expression like not f(g(input.x)) into a series of sub-expressions evaluated before the not:

__local0__ = input.x
g(__local0__, __local1__)
not f(__local1__)

If any sub-expression fails — for example, input.x is undefined or g produces an undefined result — the entire rule fails rather than the not succeeding. This is unintuitive: the user's intent is "the condition does not hold," but an undefined intermediate value causes a silent failure instead of the expected not result.

Implicit body wrapping

With import future.keywords.not, composite-expression negation wraps the full compiler expansion in an implicit body:

not { __local0__ = input.x; g(__local0__, __local1__); f(__local1__) }

Now, if any sub-expression is undefined or fails, the body is unsatisfiable and the not expression succeeds; matching the intuition that "the condition does not hold."

{
"user": "cesar"
}
package negation

import future.keywords.not

# Succeeds when input.role is undefined OR when lookup/admin fail
restricted if {
not admin(lookup(input.user))
}

groups := {
"admin": ["alice"],
"user": ["bob"]
}

lookup(user) := group if {
some group, members in groups
user in members
}

admin(group) if group in ["admin", "sudo"]
Loading...
important

Notice how if we remove the future.keywords.not import in the above policy, the restricted rule starts failing. This is a consequence of the lookup() function failing with an undefined value.

Explicit negation bodies

The import also enables a not expression to take a curly-brace-enclosed body instead of a single expression:

{
"servers": [
{
"name": "web1",
"listener": {
"port": 80,
"protocol": "tcp"
}
},
{
"name": "web2",
"listener": {
"port": 443,
"protocol": "tcp"
}
},
{
"name": "web3",
"listener": {
"port": 443,
"protocol": "udp"
}
}
]
}
package negation

import future.keywords.not

# Deny any server that doesn't listen on TCP on port 443
deny contains $"server {server.name} is misconfigured" if {
some server in input.servers
not {
# If any of the following expressions fail, the 'not' succeeds
listener := server.listener
listener.port == 443
listener.protocol == "tcp"
}
}
Loading...

The not succeeds when the body is unsatisfiable; no combination of variable bindings makes every expression in the body true.

Variables declared inside the body (listener above) are scoped locally and are not visible outside the not block.