Introduction
Authorization is complex because every app has to invent its own authorization model. Yet there are some well-worn paths which can be good starting points for most applications. This post goes through these patterns, and how an authorization platform (such as the Topaz open source project, or the Aserto authorization service) can help you implement them.
Roles as user properties
The simplest authorization pattern models a set of roles as properties of the user. These roles can be configured in the identity provider (IDP) and are often embedded as scopes in the access token generated by the IDP.
Some applications authorize entirely based on roles (or discrete permissions) embedded in access tokens. But this suffers from a few drawbacks:
- Role / permission / scope explosion: the more roles/permissions there are, the more scopes need to be embedded in the access token, leading to size problems.
- Coupling between the IDP and the app: any time a new permission gets added to the app, the code that generates additional scopes in the access token must also be revised. This is often done by the security/identity & access team that has access to the IDP, and introduces workflow complexities.
- Once issued, access tokens are hard to invalidate. The authenticated user has permissions for as long as the access token is valid, even if their role has changed since the token was issued. This in turn leads to security holes.
In this scenario, using an authorization service such as Topaz provides a few advantages:
- Adding an explicit authorization system lets the application check in real-time whether the user still has a role or permission.
- The authorization code can be lifted out of the application and expressed as a policy. This makes it easier to reason about the authorization logic across the entire application.
- Each API can have a different authorization policy that contains the logic used to authorize the operation. An example policy could be “Allow the operation if the user has the ‘admin’ or ‘editor’ roles, or the ‘create’ permission.”
- Any role changes (or the value of a global “disabled” flag on a user) can be transmitted to the authorization system in near real-time. This closes off security issues associated with blindly trusting scopes embedded in access tokens.
- The role-to-permission mapping can be done in the authorization system. As a result, the IDP only has to know about user-to-role mappings, not about permissions. This helps decouple the application from the IDP.
Group-based RBAC
The next pattern relies on groups (and group hierarchies) as a way to organize users.
Most applications have coarse-grained roles, such as “super-admin,” “admin,” “editor,” “viewer,” “billing-admin,” that determine permissions to objects across the entire tenant.
These roles are typically assigned by making a user a member of a group. The group membership means that the user has been granted a role. Groups can be organized into hierarchies. For example, the “auditor” group can include “internal-auditors” and “external-auditors.” These two groups can in turn include specific users.
This is essentially the model that LDAP and Active Directory are built around. As a consequence, most authorization systems support groups as a core part of their model.
For example, Topaz and Aserto have a built-in “group” object type. The group object type has a “member” relation type, and the target for that relation can be any subject (user or group). This model enables including groups in other groups. Checking group membership is transitive: when Topaz’s check_relation
built-in function is called with a user and a group instance, it will walk through the group hierarchy and return true
if the user is a member of the group, either directly or transitively.
The following policy (written in the Open Policy Agent’s Rego language) uses Topaz’s check_relation
built-in to evaluate whether a user is a member of a group, and allow the operation if they are:
allowed {
ds.check_relation({
"subject": { "id": input.user.id },
"relation": { "object_type": "group", "name": "member" },
"object": {
"type": "group",
"key": input.resource.key
}
})
}
Since a permission can be granted via more than one role, policies may need to check group membership for each of the corresponding groups. For example, the can-view
permission may be granted if the user is a member of any of the “viewer,” “editor,” or “admin” groups. This would be accomplished by a policy such as the following:
groups := { "viewer", "editor", "admin" }
allowed {
ds.check_relation({
"subject": { "id": input.user.id },
"relation": { "object_type": "group", "name": "member" },
"object": { "type": "group", "key": groups[_] }
})
}
But this can get complicated, and arguably just moves the complexity from the application logic to the policy. The next pattern aims to address this.
Group-based RBAC with fine-grained permissions
Permissions can be included in more than one role. In the example above, the can-view
permission is likely included in the “viewer,” “editor,” and “admin” roles. A more scalable authorization system will define a set of discrete permissions, and assign those to roles.
Authorization systems often define permissions as first-class concepts. Instead of checking whether a user is a member of a group, the policy can check whether a user has a permission.
For example, Topaz allows associating permissions with relation types (aka “roles”). It also allows roles to include other roles - for example, the “editor” role can include the “viewer” role. The following Aserto manifest file does just that. It defines a “system” object type, and underneath it there are two relation types: “editor” and “viewer.” The “editor” relation type includes all the permissions from the “viewer” relation type, and adds the can-edit
permission. The “viewer” relation type includes a single permission - can-view
.
system:
editor:
union:
- viewer
permissions:
- can-edit
viewer:
permissions:
- can-view
If a user (or group) has the “editor” role, the Topaz check_permission
built-in will return true
when evaluating whether that user has the can-view
permission. This is because the “editor” role transitively includes the “viewer” role, and therefore the can-view
permission.
Fine-grained authorization for domain-specific objects
So far we’ve been dealing with “global” roles. Many applications want to grant permissions on a set of objects that they manage. For example, a file sharing application such as Google Drive defines “folders” and “files” as object types. Folders and files can both have a parent folder. Each of these objects has a set of relations (“owner,” “editor,” “commenter,” and “viewer”) and the “owner” can grant these roles to users and groups. So rather than a global “editor” role which has edit access to every file and folder, these permissions can be assigned to discrete folders and files.
Google’s Zanzibar authorization system, which powers Google Docs and many other Google applications, implements this model. Zanzibar has inspired many authorization systems, including Airbnb’s Himeji, Carta’s AuthZ, and a few open-source implementations, including Topaz.
With Topaz, you can define domain-specific object types and relation types. Each relation type can define permissions (and/or unions of other relation types). A full example of a manifest that supports this model can be found here.
Authorization models that are built purely in the form of evaluating relationships (e.g. “viewer,” “editor”) between subjects (users and groups) and objects (e.g. folders and files) can be expressed with very simple policies:
allowed {
ds.check_permission({
"subject": { "id": input.user.id },
"permission": { "name": input.policy.path },
"object": {
"type": input.resource.type,
"key": input.resource.key
}
})
}
Combining group-based RBAC and FGA
Most real-world applications implement some combination of group-based RBAC and fine-grained authorization. Often, authorization includes checking a global role (e.g. “editor”) and then checking whether the user has access to a particular resource (e.g. a list). The user needs to satisfy both conditions to be able to edit items on that list.
Another example is a “super-admin,” who can do everything by virtue of having this role. Access checks include logic that allows users access to specific objects by virtue of relationships, as well as allowing access to users with these elevated roles.
Topaz supports these scenarios as well, since it is built on a combination of policy and relationship-based access control. To extend the previous example, we can add another “allowed” clause to this policy. This clause will allow the operation if the user has been granted a particular permission on a particular object, OR if they are a “super-admin”:
allowed {
ds.check_permission({
"subject": { "id": input.user.id },
"permission": { "name": input.policy.path },
"object": {
"type": input.resource.type,
"key": input.resource.key
}
})
}
allowed {
input.user.roles[_] == "super-admin"
}
Conclusion
We’ve covered five common authorization patterns, starting from the simplest IDP-based RBAC, and culminating in a combination of group-based RBAC with fine-grained permissions and fine-grained resources.
Topaz supports all of these models, and just as importantly, makes it easy to evolve from the simple models to the more sophisticated ones, by evolving the authorization policy.
Eventually, every successful application requires a deep set of authorization capabilities. Adopting an authorization platform like Topaz or Aserto early in your journey future-proofs your application, and makes it much easier to evolve your authorization model with your expanding requirements.
Related Content
Building a React and Node app with Aserto authorization
Adding an authorization layer to your React.js and Node.js application has never been easier! Learn how to create a role-based access control policy and how to use it to make authorization decisions in your application.
Apr 12th, 2023
Modern access control explained
The modern authorization landscape is confusing. There are two approaches to it that are growing ecosystems around them: the OPA policy-based approach and the Zanzibar data-centric approach. In this post, we describe both approaches along with pros and cons to using each.
May 17th, 2023
Cloud-native authorization on Category Visionaries
Aserto CEO, Omri Gazitt, joins Brett Stapper on the Category Visionaries podcast, a podcast that explores the visions for the future of founders who are on the front lines building it. Omri shares his entrepreneur journey and why he believes authorization is the next IAM frontier.
May 31st, 2023