Over the last couple years, authorization (AKA “authz”) has become a hot topic of debate. Proponents of various authz frameworks, libraries, and philosophies have voiced their opinions on how it should be implemented, jockeying for position to become the de facto way to implement authz. Among the contestants in this debate, Google’s Zanzibar has recently emerged as a popular way of not only modeling and enforcing authorization for modern, fine grained use cases, but also of scaling to meet the requirements of today’s large-scale, cloud-native applications.
When we started Warrant in 2021, we set out to build developer-friendly authorization infrastructure that all engineering teams could use. We knew that Warrant would be a core piece of infrastructure for our customers, so our authz service had to be (1) generic enough to model all of their use-cases and (2) scalable enough to support access checks across their authz models globally and with low latency. After reading the seminal Zanzibar paper, we decided to build Warrant’s core authorization engine based on many of the concepts described in the paper (e.g. tuples, namespaces, zookies, etc. – more on these concepts later). We believed that Zanzibar had zeroed in on a set of fundamental concepts and patterns that would help us build a generic solution to the authorization challenges of any application.
We launched Warrant to much discussion and debate and have tackled a wide variety of authorization challenges since then, helping many companies build production-ready authz. In this post, I’ll talk about why we believe Zanzibar is a great foundation for implementing authorization, discuss some areas where it falls short, and share how we’ve addressed those shortcomings with enhancements of our own.
Zanzibar provides an intuitive and (more importantly) uniform data model for representing authorization. Its authorization paradigm, known as relationship based access control (ReBAC), is based on the principle that all resources in an application are related to each other via directed relationships (e.g.
[user:123] is [owner] of [report:abc]), and the application’s authz rules (i.e. the abilities granted to users of the application) flow from these relationships either explicitly or implicitly. Representing authorization in this way feels intuitive because it’s similar to how most of us already design data models (e.g. relational database schemas) for our own applications, making it easy to understand and reason about authz models in Zanzibar. ReBAC is also extremely flexible, capable of representing any authz model you can throw at it, including other authz paradigms like role based access control (RBAC) and attribute based access control (ABAC).
“Zanzibar provides a uniform data model and configuration language for expressing a wide range of access control policies from hundreds of client services at Google[…]”
–from Zanzibar: Google’s Consistent, Global Authorization System
In practice, each relationship between two resources is represented as a “tuple” composed of three parts:
- The object (resource) on which the relationship is being specified.
- The relationship being specified.
- The subject (a resource or group of resources) that will possess the specified relationship on the object.
Together, the set of all tuples makes up a big graph of relationships in which the objects and subjects are the nodes, and the relationships between them are the edges. This graph is powerful because it can be traversed in various ways to determine the capabilities of users in an application. For example, a path between a user and a resource might mean that the user has write privileges on the resource. In another scenario, it might mean that the user is not allowed to perform writes on a different resource. To dictate how the graph can be traversed and to assign semantic meaning (for authz) to the relationships it represents, Zanzibar provides us with namespaces.
Namespaces allow us to assign meaning to the relationships represented by our graph for the purpose of authorization. Each namespace defines the available relationships (e.g. admin, writer, reader) on a type of resource (e.g. report), and optionally, a set of logical rules that specify how each relationship can be inferred from others (e.g.
an [editor] of a [report] is also a [viewer] of that report). They are similar to database schemas in that they allow us to define the structure of an authorization model, but unlike database schemas, namespaces also allow us to express logic on top of that structure. For example, the namespace for a report object type might define three relationships:
viewer. In addition to defining these relationships, the namespace can also specify:
- A subject can only have the admin relationship on a report explicitly.
- A subject can have the editor relationship on a report explicitly OR implicitly if it has the admin relationship on that report.
- A subject can have the viewer relationship on a report explicitly OR implicitly if it has the editor relationship on that report.
The ability for namespaces to specify logical rules (or policies) like these between relationships makes it possible to separate authorization logic from application logic. This makes application code much simpler. The application only needs to confirm that a user has a particular capability (e.g. editor) before executing a section of code (e.g. persisting a proposed edit on a document).
While modern, policy-driven authz solutions like Open Policy Agent (OPA) offer some of the features and benefits described so far, one thing remains unique to Zanzibar. It’s a stateful, centralized service, meaning that all tuples (the relationship graph) and namespaces are pieces of data that are stored and updated centrally. A major benefit of this design is the ability to query the data, not only to check if a particular subject has access to a specific resource, but also to get the list of resources a particular subject has access to. This is extremely useful in practice, for example, to audit a user’s privileges for regulatory compliance or to understand the impact of a change to the authorization model before applying it. In our opinion, having the ability to query permissions like this should be a requirement in any authorizaton system and can only be done in a stateful system with a global view of all authz data. However, as with any system design decision, this approach comes with its trade-offs.
Since Zanzibar stores all authorization data centrally, client applications must make requests to it to check permissions, making it a potential performance bottleneck for those applications. To minimize the end-to-end response times of authz queries, Zanzibar is distributed globally (as close to client applications as possible) and utilizes aggressive caching, responding to queries from cache (in single milliseconds) whenever possible. Because access patterns and freshness requirements for authorization data vary from application to application, Zanzibar has the concept of a “zookie” – a global, incrementing version number for each change made to the authorization data. Zookies make caching feasible while still allowing client applications to dictate when they favor correctness over speed.
While the concepts and features of Zanzibar are great, Google never built a publicly available implementation because they built Zanzibar to solve their own authorization needs for services like Drive, Docs, YouTube, and more. Fortunately, Warrant implements all of the concepts we’ve discussed so far, many of them with slight variations intended to either improve developer experience or add functionality that we feel Zanzibar lacks.
In Warrant, tuples are known as warrants. A warrant includes the same three major components (object, relationship, and subject) as a tuple but can also include an optional component that we call a policy. The policy component is a user-defined boolean expression that is evaluated at query-time to determine whether the warrant is available to the query or not. If a warrant matches a query and its policy evaluates to
true, that warrant is considered during the query. Otherwise, the warrant is ignored.
Policies can reference dynamic contextual data that is passed in by the query (e.g.
[user:123] is an [approver] of [transaction:abc] [if transaction.amount <= 100]). Having the ability to do this makes Warrant capable of modeling attribute based access control (ABAC) scenarios in which external data (e.g. the transaction amount) is required to make an authorization decision, something Zanzibar’s purely ReBAC approach struggles with. You can learn more about warrants in our documentation.
namespaces : tuples :: object types : warrants
Namespaces, as described in Zanzibar, are known as object types in Warrant. Object types are represented as JSON and conform to a JSON schema specification. Unlike Zanzibar’s namespaces, object types support the ability to restrict the types of objects that can possess each relationship, making it easier for developers to reason about the scope of each object type’s relationships. Warrant also provides various pre-built object types to standardize and simplify the implementation of common authorization use cases like RBAC, feature entitlements, and multi-tenancy within Zanzibar’s ReBAC paradigm. You can learn more about object types and the various built-in models Warrant offers in our documentation.
More than two years after choosing to build Warrant atop Zanzibar’s core principles, we’re extremely happy with our decision. Doing so gave us a solid technical foundation on which to tackle the various complex authorization challenges companies face today. As we continue to encounter new scenarios and use cases, we’ll keep iterating on Warrant to ensure it’s the most capable authorization service. To share what we learn and what we build with the developer community, we recently open-sourced the core authorization engine that powers our fully managed authorization platform, Warrant Cloud. If you’re interested in authorization (or Zanzibar), check it out and give it a star!