Authorization:
it requires a whole lot more
than relationships
Every good movement has a foundation story. For Authorization as a Service, that story is the Google Zanzibar whitepaper. Its publication in 2019 inspired companies like AirBnB and Carta to build their own internal authorization systems in its image. It also spawned a small but growing marketplace of companies who provide Zanzibar-like authorization services for development teams to integrate into their applications. There are a lot of great reasons for Zanzibar’s popularity. One of the more frequently cited is its flexibility. Flexibility is indeed important for an authorization service, which needs to be able to model all sorts of application logic and represent a variety of data.
There’s just one problem: Google Zanzibar isn’t flexible.
I understand why people say it is. If Zanzibar can manage authorization for everything at Google, then it seems to follow that it must be flexible. But it doesn’t. Take that haiku up there as an illustration. The haiku can express anything you can imagine. In the hands of a master, a haiku can describe nature, exclaim joy, ponder mortality, relate history. But the fact that the haiku is expressive doesn’t mean that it’s flexible. That structure is rigid. Three lines. Five syllables, seven syllables, five syllables. If you’re going to write a haiku, you’re going to follow those rules. Period.
Zanzibar models authorization as relations between objects and users. This is a form of Relationship-Based Access Control (ReBAC) and if you want to build authorization in Zanzibar, you’re going to build it using its implementation of ReBAC. Period. This manifests in the way that Zanzibar represents authorization data and in how it models authorization logic.
Authorization data in Zanzibar: the relation tuple
The fundamental unit of data in Zanzibar is the relation tuple. A relation tuple looks like this.
# A tuple consists of an object, a relation, and a user
<tuple> ::= '<object>'#'<relation>'@'<user>'
Every relation tuple defines a relation between an object and a user (except when it doesn’t - we’ll get to that in a bit).
The different pieces of the tuple are broken down further.
# An object is defined by a namespace (e.g. doc, folder) and an ID
<object> ::= '<namespace>':'<object_id>'
# A user can be either a single user ID or a userset (collection of users)
<user> ::= <user_id> | <userset>
# A userset is defined by an object and a relation
# i.e. the group of all users with the specified relation on the given object
<userset> ::= '<object>'#'<relation>'
What this means is that the relation tuple doesn’t just impose a structure on data. It also assigns meaning. The different parts of the relation tuple have intrinsic meaning to Zanzibar. The first item must be an object. The last item must either be a user id or an '<object>'#'<relation>'
pair that resolves to a set of user IDs.
The problem with this is that some authorization data can’t be expressed as a relation between an object and a group of users. Google couldn’t even get out of their own whitepaper before they ran into this and modified the userset to accept an object with no relation. This notation is '<object>'#...
, and it exists to allow Zanzibar to express object-object relationships, like the relationship between a document and its containing folder. This is a pretty fundamental notion in authorization, but it required diverging from the rigid relation tuple definition (and that divergence renders the name ‘userset’ suspect).
Even with this revision, there remain things that you just can’t express with relation tuples. Suppose you want to set up an administrator role that has edit access on all documents, whether they exist today or not. Again, this is a pretty common requirement. Too bad you can’t do it with relation tuples! There’s no ...
corollary for the object part of the spec. You MUST specify an object ID - that is, a single object. You can grant access to each document, but you can’t grant access to all documents. So if you want to implement logic that gives some set of users a permission on all objects of a given type, you have to build some sort of janky workaround to reimplement “all objects” as “each object.”
Authorization logic in Zanzibar: the userset rewrite
While it’s possible to store relation tuples for every authorization-relevant relationship, to do so would be impractical. Imagine if you had to store a tuple for every folder in a deep hierarchy in order to express that the permissions on the root folder propagate down to all the children. That would get old in a hurry. Instead, Zanzibar lets you derive relations from other relations.
Zanzibar clients define their objects and relations in namespace configurations. Here’s an example namespace configuration from the whitepaper (don’t sweat the details too much).
name: "doc"
relation { name: "owner" }
relation {
name: "editor"
userset_rewrite {
union {
child { _this {} }
child { computed_userset { relation: "owner" } }
} } }
relation {
name: "viewer"
userset_rewrite {
union {
child { _this {} }
child { computed_userset { relation: "editor" } }
child { tuple_to_userset {
tupleset { relation: "parent" }
computed_userset {
object: $TUPLE_USERSET_OBJECT # parent folder
relation: "viewer"
} } }
} } }
There’s a lot of syntax (and curly braces) here, but all it’s doing is defining the relations that exist on the doc
object type. There are three: owner
, editor
, and viewer
. The namespace configuration provides a userset_rewrite
directive that allows clients to construct usersets from other usersets. In the above namespace configuration, the editor
userset contains the owner
userset:
relation {
name: "editor"
userset_rewrite {
union {
# All users who are directly assigned the "editor" relation
child { _this {} }
# All users who are assigned the "owner" relation
child { computed_userset { relation: "owner" } }
} } }
The viewer
userset contains the editor
userset and the viewer
userset from the document’s parent
folder.
relation {
name: "viewer"
userset_rewrite {
union {
# All users who are directly assigned the "viewer" relation
child { _this {} }
# All users who have the "editor" relation
# (this includes users who are assigned the "owner" relation)
child { computed_userset { relation: "editor" } }
# All users who have the "viewer" relation on the folder
# that has the "parent" relation with the given doc.
child { tuple_to_userset {
tupleset { relation: "parent" }
computed_userset {
object: $TUPLE_USERSET_OBJECT # parent folder
relation: "viewer"
} } }
} } }
This is how Zanzibar expresses authorization logic: as rules that define relations in terms of other relations. There’s a subtle but meaningful bit of indirection here. Zanzibar is a tool for defining relations, but authorization is about granting permission.
When you authorize a request, the question you need to answer is “does this user have permission to perform this action on this resource?” With Zanzibar and systems that are based on it, you can’t ask that question. Instead, you have to rephrase it as “does this user have this relation on this object?”
That rephrasing is okay when there’s a 1:1 mapping between the action a user is attempting and their relation with the object: view
and viewer
, edit
and editor
. But that isn’t generally the case. A document owner
might be able to delete
, transfer ownership
, archive
, and all sorts of other things.
Let’s say a user makes a request to delete a document. You know 3 pieces of information:
- the user
- the action they’re attempting (delete)
- the document they’re trying to delete
If the only question you can ask your authorization service is “What relation does the user have to this document?”, you have two options for authorizing the request:
- Create a relation for every action you want to authorize (
deleter
,ownership transferer
,archiver
) to preserve the 1:1 mapping from relation to action. - Keep track of the mapping between relations and actions somewhere outside of Zanzibar.
The second largely defeats the purpose of an authorization service. Since Zanzibar will only tell you whether the user has a given relation on the document, you have to add logic to your application that defines which relations have permission to delete a document. But that’s pure authorization logic! What’s the point of the service if you have to do that?
A lot of people seem to have reached this conclusion and consequently the literature about ReBAC is rife with “relations” like can_delete
and can_archive
. This is essentially the same thing as creating a relation for every authorized action, except that can_delete
and can_archive
aren’t relations. They’re permissions. That may sound like a needlessly subtle distinction, but it’s exactly the sort of thing that happens when you have to work around the limitations of an inflexible system that doesn’t let you do what you really want to do. You want to grant permissions. Your authorization service only lets you define relations. So you just pretend permissions are relations and get on with your day. The fact that this is so common in ReBAC is also an indication that relations are the wrong abstraction for authorization.
Expressive: yes. Flexible: no.
Don’t get me wrong - Google Zanzibar is an impressive piece of technology. It truly does handle authorization for just about all of Google’s apps. It operates at scales that most of us never even have to consider and maintains over 99.999% availability.
So what’s the big deal? Who cares if Zanzibar is flexible, as long as it’s expressive enough to get the job done? Setting aside the fact that there are jobs that it can’t do (like granting a permission on all objects of a given type), the problem is that meaning is already difficult enough to express without putting needless constraints on the language that you use to express it. The more you constrain the language, the harder it is to say exactly what you mean.
Let’s look at my haiku again.
Authorization:
it requires a whole lot more
than relationships
Does this mean that your authorization policy will need to express things that aren’t relationships, or does it mean that you’ll need to invest more time and energy in your authorization policy than you do your friends and family? It’s the former - as much as we love authorization at Oso, even we don’t want you to neglect the people in your life for it. But it was a whole lot easier to make that clear when I didn’t have to work within the constraints of a haiku.
When the thing you’re trying to write is a silly poem in a blog post about authorization, that isn’t a big deal. But when it’s your application’s authorization policy, the consequences of not being able to say what you mean are more severe. At best, you’ll constantly be dealing with the cognitive overhead of context switching between the thing you want to express (authorization logic) and the thing you’re forced to express (object-user relations). At worst, you’ll get your authorization logic wrong and you won’t notice it until someone gets access to something they shouldn’t.
If you’re still not convinced that any of this matters, that’s fair, but I’d like you to do a little experiment. The next time you need to make a case for using an authorization service, present it in haiku. Then hop over to our Community Slack and let us know how it goes!