SpiceDB Documentation
Concepts
Relationship Caveats

Caveats

Caveats are a feature within SpiceDB that allows for relationships to be defined conditionally: the relationship will only be considered present if the caveat expression evaluates to true.

Caveats allow for an elegant way to model dynamic policies and ABAC-style (Attribute Based Access Control) decisions while still providing scalability and performance guarantees.

Defining Caveats

Caveats are named expressions that are defined in schema alongside definitions for object types. A caveat definition includes a name, one or more well-typed parameters, and a CEL expression (opens in a new tab) returning a boolean value.

Here's schema snippet demonstrating what a simple caveat looks like:

caveat first_caveat(first_parameter int, second_parameter string) {
  first_parameter == 42 && second_parameter == "hello world"
}

Parameter Types

The following table documents the CEL types available for values in caveat expressions:

TypeDescription
anyany value is allowed; useful for types that vary
int64-bit signed integer
uint64-bit unsigned integer
boolboolean
stringutf8-encoded string
doubledouble-width floating point number
bytessequence of uint8
durationduration of time
timestampspecific moment in time (typically UTC)
list<T>generic sequence of values
map<T>generic mapping of strings to values
ipaddressspicedb-specific type for IP addresses

Developers looking for the SpiceDB code that defines of these types can find them in the pkg/caveats/types module (opens in a new tab).

Some Examples

Basic comparison

caveat is_tuesday(today string) {
   today == 'tuesday'
}

Attribute Matching

The example below defines a caveat that requires that any expected attributes found within the expected map are a subset of the attributes in the provided map:

caveat attributes_match(expected map<any>, provided map<any>) {
   expected.isSubtreeOf(provided)
}

IP address checking

The example below defines a caveat that requires that a user’s IP address is within a specific CIDR range:

caveat ip_allowlist(user_ip ipaddress, cidr string) {
  user_ip.in_cidr(cidr)
}

Allowing caveats on relations

To allow a caveat to be used when writing a relationship, the caveat must be specified on the relation within the schema via the with keyword:

definition resource {
  relation viewer: user | user with ip_allowlist
}

In the above example, a relationship can be written for the viewer relation to a user without a caveat OR with the ip_allowlist caveat.

To make the caveat required, the user | can be removed.

Writing relationships with caveats and context

When writing a relationship for a relation, both the caveat and a portion of the “context” can be specified:

WriteRelationshipsRequest {
  Updates: [
    RelationshipUpdate{
      Operation: CREATE
      Relationship: {
        Resource: …,
        Relation: "viewer",
        Subject: …,
        OptionalCaveat: {
           CaveatName: "ip_allowlist",
           Context: structpb{ "cidr": "1.2.3.0" }
        }
      }
    }
  ]
}

A few important notes:

  • The Context of a caveat is defined both by the values written in the Relationship, as well as those provided in the CheckPermissionRequest: if empty, then only the context specified on a CheckPermission request will be used. Otherwise, the values in the Relationship take precedence over those in the CheckPermissionRequest.
    • Context of a caveat provided in Relationship is stored alongside the relationship and is provided to the caveat expression at runtime. This allows for partial binding of data at write time.
  • The Context is a structpb, which is defined by Google and represents JSON-like data: https://pkg.go.dev/google.golang.org/protobuf/types/known/structpb (opens in a new tab)
    • To send 64-bit integers, encode them as strings.
  • A relationship cannot be duplicated, with or without a caveat, e.g. two relationships that differ only on their use of a caveat cannot both exist.
  • When deleting a relationship, a caveat does not need to be specified; the matching relationship will be deleted if present.

Issuing Checks

When issuing a CheckPermission request (opens in a new tab), additional caveat context can be specified to represent the known context at the time of the check:

CheckPermissionRequest {
  Resource: …,
  Permission: …,
  Subject: …,
  Context: { "user_ip": "1.2.3.4" }
}

The check engine will automatically apply the context found on the relationships, as well as the context provided by the CheckPermission call, and return one of three states (opens in a new tab):

  • PERMISSIONSHIP_NO_PERMISSION - subject does not have the permission on the resource
  • PERMISSIONSHIP_HAS_PERMISSION - subject has permission on the resource
  • PERMISSIONSHIP_CONDITIONAL_PERMISSION - required context is missing to determine permissionship

In the case of PERMISSIONSHIP_CONDITIONAL_PERMISSION, SpiceDB will also return the missing context fields in the CheckPermissionResponse (opens in a new tab) so the caller knows what additional context to fill in if they wish to rerun the check and get a determined answer.

LookupResources and LookupSubjects

Similarly to CheckPermission, both LookupResources and LookupSubjects can be provided with additional context and will return one of the two permission states for each of the results found (either has permission or conditionally has permission).

Full Example

A full example of a schema with caveats can be found below, which allows users to view a resource if they are directly a viewer or they are aviewer within the correct IP CIDR range:

Schema

definition user {}
 
caveat has_valid_ip(user_ip ipaddress, allowed_range string) {
  user_ip.in_cidr(allowed_range)
}
 
definition resource {
    relation viewer: user | user with has_valid_ip
    permission view = viewer
}

Write Relationships

WriteRelationshipsRequest {
  Updates: [
    RelationshipUpdate{
      Operation: CREATE
      Relationship: {
        Resource: {
            ObjectType: "resource",
            ObjectId: "someresource",
        },
        Relation: "viewer",
        Subject: {
            ObjectType: "user",
            ObjectId: "sarah",
        },
        OptionalCaveat: {
           CaveatName: "has_valid_ip",
           Context: structpb{ "allowed_range": "10.20.30.0" }
        }
      }
    }
  ]
}

Check Permission

CheckPermissionRequest {
    Resource: {
        ObjectType: "resource",
        ObjectId: "someresource",
    },
    Permission: "view",
    Subject: {
        ObjectType: "user",
        ObjectId: "sarah",
    },
    Context: { "user_ip": "10.20.30.42" }
}

Validation with Caveats

The Assertions and Expected Relations definitions for validation of schema support caveats as well.

Assertions

Caveated permissions can be checked in assertions by the addition of the assertCaveated block:

Assertions for caveated permissions

To assert that a permission does or does not exist when some context it specified, the with keyword can be used to provide the context:

Assertions for caveated permissions with context

Expected Relations

Expected relations notes if a subject is caveated via the inclusion of the [...] string on the end of the subject:

Expected Relations with caveats

Expected Relations does not evaluate caveats, even if the necessary context is fully specified on the relationship.

This means that a caveated subject that might actually return HAS_PERMISSION will appear as subject[...] in expected relations

© 2024 AuthZed.