Relationships
Relationships bind together two Objects (a Subject and a Resource) via a Relation. The concept behind ReBAC authorization systems is that by following a chain of relationships, one can determine access.
In SpiceDB, a functioning Permissions System is the combination of Schema, which defines the structure of data, and Relationships which are the data.
Understanding Relationships
At its core, authorization logic fundamentally reduces to asking a single question:
Is this actor allowed to perform this action on this resource?
This question can be broken down into core components:
Is this actor allowed to perform this action on this resource?
/¯¯¯¯¯¯¯/ /¯¯¯¯¯¯¯¯¯/ /¯¯¯¯¯¯¯¯¯¯¯/
object permission or object
(subject) relation (resource)
In SpiceDB parlance, this actor and this resource are both Objects and this action is a Permission or Relation (consider them equivalant for now). By focusing solely on these components of the question, it becomes clear that this is the same structure as a Relationship: two Objects and a Relation.
The power of ReBAC comes from transforming this basic question into another one:
Does there exist a chain of relationships starting at this resource through this relation that ultimately reaches this subject?
This new question is one of graph reachability (opens in a new tab). General purpose graph databases optimize for a couple different types of graph traversals; usually breadth-first search (opens in a new tab) and depth-first search (opens in a new tab), but ReBAC systems are optimized for scalably and efficiently computing reachability along with all of the assumptions that come with the domain of authorization.
The syntax used for relationships in the paper that popularized ReBAC is as follows:
document:readme#editor@user:emilia
Here it is with labels explaining each section around the delimiters:
resource subject
ID type
\ˍˍˍˍˍ\ \ˍˍ\
document:readme#editor@user:emilia
/¯¯¯¯¯¯¯/ /¯¯¯¯¯/ /¯¯¯¯¯/
resource permission subject
type or relation ID
In a real system, Object IDs are most likely a computer-friendly string than something human readable. Many use-cases use UUIDs or unsigned integers representing the primary key from that data's canonical datastore.
Users are no exception to this pattern and can be represented in various ways, such as the sub
field of an JWT from an Identity Provider.
Regardless of their representation, Object IDs must be unique and stable within the set of IDs for an Object Type.
Relationships are powerful because of their duality: they are both the question and, when written in aggregate, the answer.
Writing Relationships
It is the application's responsibility to keep the relationships within SpiceDB up-to-date and reflecting the state of the application; how an application does so can vary based on the specifics of the application, so below we outline a few approaches.
Want to learn more about writing relationships to SpiceDB, the various strategies and their pros and cons?
Read our blog post about writing relationships (opens in a new tab).
SpiceDB-only relationships
Sometimes an application does not even need to store permissions-related relationships in its relational database.
Consider a permissions system that allows for teams of users to be created and used to access a resource. In SpiceDB's schema, this could be represented as:
definition user {}
definition team {
relation member: user
}
definition resource {
relation reader: user | team#member
permission view = reader
}
In the above example, the relationship between a resource and its teams, as well as a team and its members does not need to be stored in the application's database at all.
Rather, this information can be stored solely in SpiceDB, and accessed by the application via a ReadRelationships (opens in a new tab) or ExpandPermissionsTree (opens in a new tab) call when necessary.
Two writes & commit
The most common and straightforward way to store relationships in SpiceDB is to use a 2 phase commit-like approach, making use of a transaction from the relational database along with a WriteRelationships (opens in a new tab) call to SpiceDB.
try:
tx = db.transaction()
# Write relationships during a transaction so that it can be aborted on exception
resp = spicedb_client.WriteRelationships(...)
tx.add(db_models.Document(
id=request.document_id,
owner=user_id,
zedtoken=resp.written_at
))
tx.commit()
except:
# Delete relationships written to SpiceDB and re-raise the exception
tx.abort()
spicedb_client.DeleteRelationships(...)
raise
Streaming commits
Another approach is to stream updates to both a relational database and SpiceDB via a third party streaming system such as Kafka (opens in a new tab), using a pattern known as Command Query Responsibility Segregation (opens in a new tab) (CQRS)
In this design, any updates to the relationships in both databases are published as events to the streaming service, with each event being consumed by a system which performs the updates in both the database and in SpiceDB.
Asynchronous Updates
Before adopting an asynchronous system, you should deeply consider the consistency implications.
If an application does not require up-to-the-second consistent permissions checking, and some replication lag in permissions checking is acceptable, then asynchronous updates of the relationships in SpiceDB can be used.
In this design, a synchronization process, typically running in the background, is used to write relationships to SpiceDB in reaction to any changes that occur in the primary relational database.