SpiceDB Documentation
Concepts
Schema Language

Schema Language

A SpiceDB schema defines the types of objects found, how those objects relate to one another, and the permissions that can be computed off of those relations.

The schema language's extension for use on a file system is .zed.

You can experiment with schemas in real-time with the Playground (opens in a new tab).

The following video provides an overview of schema development using the Playground:


After studying this page's content, you may want to also read the Developing a Schema doc.

Definitions

The top level of a Schema consists of zero or more Object Type definitions and Caveats.

/**
 * somecaveat is a caveat defined
 */
caveat somecaveat(someparameter int) {
    someparameter == 42
}
 
/**
 * someobjecttype is some type that I've decided to define
 */
definition someobjecttype {}

Note that the examples are unprefixed. You'll need to add the prefix from your permissions system if calling WriteSchema for a permissions system hosted in Serverless.

Object Type Definitions

An Object Type definition is used to represent a new type of object. Objects are how SpiceDB represents instances of resources or subjects.

It might help to think about Object Type definitions as similar to a class definition in an object-oriented programming language or a table definition in a SQL database.

Caveat Definitions

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 at query time.

See the Caveats documentation to learn more.

Relations

A relation defines how two objects (or an object and subject) can relate to one another. For example, a reader on a document, or a member of a group.

Relations are always defined with a name and one (or more) allowed types of objects that can be the subjects of that relation:

/**
 * user represents a user
 */
definition user {}
 
/**
 * document represents a document in the system
 */
definition document {
    /**
     * reader relates a user that is a reader on the document
     */
    relation reader: user
}

In the above example, the user on reader does not contain a sub-relation

Occasionally you will see a subject which has a sub-relation such as usergroup:admins#members which refers not just to the usergroup as a whole, but the set of members which have that relation to the usergroup.

Subject Relations

Relations can also "contain" references to other relations/permissions.

For example, a group's member relation might include the set of objects marked as member of another group, indicating that the other group's members are, themselves, members of this group:

definition user {}
 
definition group {
    /**
     * member can include both users and *the set of members* of other specific groups.
     */
    relation member: user | group#member
}

Subject Relations are useful in RBAC-style permissions systems to grant "roles" to sets of subjects, such as the members of a team.

Wildcards

Relations can also specify wildcards to indicate that a grant can be made to the resource type as a whole, rather than a particular resource. This allows public access to be granted to a particular subject type.

For example, a viewer might indicate that all users can be granted the ability to view the resource:

definition user {}
 
definition resource {
    /**
     * viewer can be granted to a specific user or granted to *all* users.
     */
    relation viewer: user | user:*
}

To be made public, the wildcard relationship would be written linking the specific document to any user:

resource:someresource viewer user:*

Now any user is a viewer of the resource.

⚠️

Be very careful with wildcard support in your schema! Only grant it to read permissions, unless you intend to allow for universal writing.

Naming Relations

Relations define how one object relates to another object/subject, and thus relations should be named as nouns, read as {relation name} (of the object).

Examples:

NameRead as
readerreader of the document
writerwriter of the document
membermember of the group
parentparent of the folder

Permissions

A permission defines a computed set of subjects that have a permission of some kind on the parent object. For example, is a user within the set of users that can edit a document.

Permissions are always defined with a name and an expression defining how that permission's allowed set of subjects is computed:

definition user {}
 
definition document {
    relation writer: user
    relation reader: user
 
    /**
     * edit determines whether a user can edit the document
     */
    permission edit = writer
 
    /**
     * view determines whether a user can view the document
     */
    permission view = reader + writer
}

Operations

Permissions support four kinds of operations: union, intersection, exclusion and arrow.

+ (Union)

Unions together the relations/permissions referenced

Union is the most common operation and is used to join different relations or permissions together to form a set of allowed subjects.

For example, to grant a permission to both the readers and writers of a document:

permission combined = reader + writer

& (Intersection)

Intersects the set of subjects found for the relations/permissions referenced

Intersection allows for a permission to only include those subjects that were found in both relations/permissions.

For example, to grant a permission to a user that is both a reader and a writer of a document:

permission read_and_write = reader & writer

- (Exclusion)

Excludes the set of subjects found for the right side relation/permission from those found in the left side relation/permission

Exclusion allows for computing the difference between two sets of relations/permissions

For example, to grant a permission to a user that is reader but not the writer of a document:

permission can_only_read = reader - writer

-> (Arrow)

Arrows allow for "walking" the heirarchy of relations (and permissions) defined for an object of a subject, referencing a permission or relation on the resulting subject's object.

For example, imagine a schema where a document is found under a folder:

definition user {}
 
definition folder {
    relation reader: user
}
 
definition document {
    /**
     * parent_folder defines the folder that holds this document
     */
    relation parent_folder: folder
}

We likely want to allow any reader of the folder to also be a reader of the document.

To accomplish this, we can use the arrow operator to walk to the parent_folder's read permission, thus including any subjects found there as well:

definition user {}
 
definition folder {
    relation reader: user
    permission read = reader
}
 
definition document {
    relation parent_folder: folder
 
    /**
     * read defines whether a user can read the document
     */
    permission read = parent_folder->read
}

The expression parent_folder->read indicates to "walk" from the parent_folder of the document, and then to include the subjects found for the read permission of that folder.

Making use of a union, we can also include the local reader relation, allowing the read permission on a document can check whether a user is a reader of a document or a reader of its parent folder.

definition user {}
 
definition folder {
    relation reader: user
    permission read = reader
}
 
definition document {
    relation parent_folder: folder
    relation reader: user
 
    /**
     * read defines whether a user can read the document
     */
    permission read = reader + parent_folder->read
}

It is recommended that the right side of all arrows refer to permissions, instead of relations, as this allows for easy nested computation, and is more readable.

Subject relations and Arrows
⚠️

Arrows operate on the object of the subject(s) found on a relation. They do not operate on the relation/permission of a subject, even if the subject refers to a relation or permission.

For example, in:

defintion resource {
  relation parent: group#member
  permission someperm = parent->something
}

parent->something refers to the something permission on the group, and #member will be ignored for the arrow.

It is recommended to not use arrows over relations that allow for subject relations without noting that fact via a comment.

Why is this the case? In one word: performance. If arrows operated over the subject's relation or permission, a full LookupSubjects call would be necessary for the arrow to correctly "walk", which would make these CheckPermission requests potentially incredibly expensive.

.any (Arrow)

.any is an alias for the standard arrow operation. Arrows allow for "walking" the hierarchy of a subject object's relations and permissions and reference a permission or relation on the resulting subject's object.

parent_folder.any(read) is equivalent to parent_folder->read:

definition user {}
 
definition folder {
    relation reader: user
    permission read = reader
}
 
definition document {
    relation parent_folder: folder
    relation reader: user
 
    permission read = reader + parent_folder->read
    permission read_same = reader + parent_folder.any(read)
}

.all (Intersection Arrow)

.all defines an intersection arrow: Similar to the standard arrow, it walks over all subjects on the referenced relation to a referenced permission/relation. Unlike the standard arrow, intersection arrow requires that all subjects found on the left side of the arrow have the requested permission/relation.

For example, imagine a schema where a document is viewable by a user if they are a member of any group for the document:

definition user {}
 
definition group {
  relation member: user
}
 
definition document {
  relation group: group
  permission view = group->member
}

If the goal was to instead allow documents to be viewable only if the user is a member of all the document's groups, the intersection arrow operator (.all) could be used:

definition user {}
 
definition group {
  relation member: user
}
 
definition document {
  relation group: group
  permission view = group.all(member)
}

In the above example, the user must be in the member relation for all groups defined on the group relation of a document in order to have the view permission.

⚠️

Intersection arrows can impact performance since they require loading all results for the arrow. This is especially a concern for arrows that traverse relationship graphs with a high branching factor. results for the arrow, it can impact performance if the arrow is very wide.

Naming Permissions

Permissions define a set of objects that can perform an action or have some attribute, and thus permissions should be named as verbs or nouns, read as (is/can) {permission name} (the object).

Examples:

NameRead as
readcan read the object
writecan write the object
deletecan delete the object
memberis member of the object

You'll note that we also used member above in the relation example. Defining member as a permission might be found when you have multiple "ways" a subject can be a member of a resource, thus changing it from a simple relation to a computed set of subjects.

Comments

Documentation Comments

It is highly recommended to put doc comments on all definitions, relations and permissions.

/**
 * something has some doc comment
 */

Non-doc comments

// Some comment
/* Some comment */

Full Example

Common Patterns

Group membership

Apply specific users or members of a group to a permission on an object type.

In this example, a group can have users as admins and as members. Both admins and members are considered to have membership in the group. A role can be applied to individual users and groups. All individually applied users as well as members for applied groups will have the allowed permission.

Global admin permissions

Given an organizational hierarchy of objects where (regular) admin users may exist for a single level of the hierarchy, apply permissions for a set of super-admin users that span across all levels of the hierarchy.

In lieu of adding a super_admin relation on every object that can be administered, add a root object to the hierarchy, in this example platform. Super admin users can be applied to platform and a relation to platform on top level objects. Admin permission on resources is then defined as the direct owner of the resource as well as through a traversal of the object hierarchy to the platform super admin.

Synthetic relations

Relation traversals can be modeled using intermediate, synthetic relations.

Given the example hierarchy, portfolio can have folders, folders can have documents, we’d like a viewer of a portfolio to also be able to read documents contained in its folders. The read on documents could be thought of as:

reader + parent_folder->reader + parent_folder->parent_portfolio->read

Synthetic relations can simulate multiple walks across permissions and relations.

Recursive permissions

Given a nested set of objects, apply a permission on an object to its descendant objects.

In this example, a folder can have users with read permission. Additionally, users that can read the parent folder can also read the current folder. Checking read permission on a folder will recursively consider these relations as the answer is computed.

Note that since parent->read calls the same read permission, it will form a recursive lookup across the chain of parent folder(s).

Recursive permissions across different resource types

If a non-recursive resource is used as the starting point for a recursive lookup, it is very important that the permission name used on the right side of the arrow is the same in both the starting resource type and the parent resource type(s):

© 2024 AuthZed.