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:
Name | Read as |
---|---|
reader | reader of the document |
writer | writer of the document |
member | member of the group |
parent | parent 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 reader
s and writer
s 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:
Name | Read as |
---|---|
read | can read the object |
write | can write the object |
delete | can delete the object |
member | is 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):