Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC

IntroductionI recently began diving into Authentication and Authorization systems. While I always knew they were incredibly complex, I was honestly taken aback by just how intricate they can be, especially when you add real-world requirements into the …


This content originally appeared on Level Up Coding - Medium and was authored by Sanil Khurana

Introduction

I recently began diving into Authentication and Authorization systems. While I always knew they were incredibly complex, I was honestly taken aback by just how intricate they can be, especially when you add real-world requirements into the mix, and how difficult it can be to build a good Authorization system that is scalable(not just to handle a high number of users, but with changes in functionality), consistent, and accurate.

Take GitHub as an example: users can belong to organizations and have different permissions on each repository and even different access levels within teams. Then, add Personal Access Tokens (PATs) for individual user authentication, API rate limits, and long-lived vs. short-lived credentials, each PAT can have its own set of permissions which are derived from the user’s permissions, which are also derived from the organization permissions — it quickly becomes pretty complex.

Ensuring consistency across these permissions, while managing updates in real-time, adds another layer of complexity that’s hard to understand until you’re deep in the weeds.

To make sense of all this, I started diving into Authorization more deeply and learned quite a lot — more than I initially expected. I learned how to build proper Authorization (AuthZ) models, how to avoid common pitfalls, how to ensure consistency and accuracy.

This post is my attempt to distil that journey into a 15-minute read. Starting with what is Authorization, we will look at the three most common Authorization Modelling techniques, RBAC, ReBAC, and ABAC and understand the trade-offs between them. The best way to do this will be to have a sample application we will build on the way.

By the end, you’ll have a clearer understanding of how to think about Authorization and why it’s one of the most critical yet underappreciated components of modern systems.

Authentication vs Authorization?

Before diving into the complexities of Authorization, let’s start with the absolute basics. I know this question is the beginning of every blog or YouTube video on the subject, but it’s because it’s one of the most fundamental concepts you must understand: Authentication ≠ Authorization.

Authentication answers the question of who is making the request, while Authorization answers the question of what that user is allowed to do.

Let’s look at a real-world example to illustrate this: a bank.

Anyone can walk into a bank, but before withdrawing money, they need to prove their identity. This might involve showing an ID or some other form of verification. This process is Authentication — it confirms who the person is.

Just because I’ve shown my ID doesn’t mean I can withdraw money from any account. I can only withdraw from my account, or maybe perform other actions like resetting my debit card PIN. This is where Authorization comes into play — it determines what I’m allowed to do. Even though I’m authenticated as myself, I can’t take money out of someone else’s account.

In technical terms, the Authorization question typically takes the form: “Can Actor-X do Action-Y on Resource-Z?” So when I attempt to withdraw money, the system is really asking: “Can Sanil perform CashWithdrawal on BankAccount:Sanil’s Account?”

Throughout this post, we’ll frame all Authorization questions using three key elements: an Actor, an Action, and a Resource and we will think of Authorization as the answer to a question of the format, “Can Actor-X do Action-Y on Resource-Z?”

Now that we’ve covered the basics, let’s jump right in!

An Example Application

It’s always easier to understand anything with an example, so let’s take an example we can all relate to, Github. We will start simple, with just a couple of entities,

  1. Repositories
  2. Users

For now, we will consider all repositories to be private. Each repository will have a list of users that can read the repository, a list of users that can update the repository and a list of users that can delete the repository.

And see how we can build an authorization system. Over time, we will expand our functional requirements, and figure out how to adapt our system.

A Simple way to do Authorization

The simplest way to build authorization is to add permissions as part of our entity model and check authorization with if conditions in our application code.

class RepositoryService {
constructor() {}

getRepository(
repository,
user
) {
if (repository.usersAllowedToGet().contains(user.getID())) {
// User can fetch/get the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}

updateRepository(
repository,
user
) {
if (repository.usersAllowedToUpdate().contains(user.getID())) {
// User can update the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}

deleteRepository(
repository,
user
) {
if (repository.usersAllowedToDelete().contains(user.getID())) {
// User can delete the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}
}

While this system will work fine for our current app, it is not scalable. As more product requirements come in, our Repository model will start to get more and more complex. For example, if we want to add an option to configure the repository’s settings, we will have to add another attribute to the repository model,

    configureRepositorySettings(
repository,
user
) {
if (repository.usersAllowedToConfigure().contains(user.getID())) {
// User can configure the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}

This will also come with database schema changes, and migrations, which are never fun. Adding a new user will also be tiresome, you need to define what are the different features the user is allowed to perform, such as is the user allowed to read a repository, or is a user allowed to update a repository.

How to build a good Authorization system

The above model is not scalable, let’s define the attributes of a good authorization system,

  1. It should be scalable as new product requirements come in — Over time, new entities, features, and product use cases will arise and our authorization model should be able to adapt to these changes. If we later decide to add “Organizations” for example, we should be able to model it with our present authorization model.
  2. Adding new entities, such as users and repositories, should be simple — Adding a new user or a new repository shouldn’t require making changes across multiple tables. For example, in the authz model above, adding a new administrative user will require adding the user in multiple tables.
  3. Writing permissions, and what each user is allowed to do in code is cumbersome, error-prone and tightly-coupled — It’s very easy for engineers to make mistakes in code. We need to define permissions in a more declarative way to make them easy to understand.

Let’s see how we can do this.

Authorization Modelling

So, we have understood a couple of things, one, we can’t model permissions in the entity, it's too cumbersome, error-prone and tightly coupled. And it gets horrible as the number of features increases.

We need a declarative way to define authorization for the entire system,

RBAC

RBAC is simply a logical iteration of what we were discussing earlier. Instead of defining the permissions for each repository, we define strict roles in our repositories and bucket permissions in them. So, instead of having logical checks for updates, deletes, and fetches, we can group these different permissions into a buckets of admins, and members.

In our example, let’s define two roles, admin and member. Each repository just contains the admins of the repo, and the members. Admins can perform some operations such as deleting a repository, while members can perform other actions, such as fetching the repository. We can write all of this declaratively in a JSON file -

{
"permissions": {
"repository": {
"getRepository": ["admin", "member"],
"updateRepository": ["admin"],
"deleteRepository": ["admin"],
"configureRepositorySettings": ["admin"]
}
}
}

We can map User roles to a repository in our database -

This simplifies our application logic a lot,

class RepositoryService {
constructor() {}

getRepository(
repository,
user
) {
userRepositoryObj = UserRepository.get({
userID: user.getID(),
repositoryID: repository.getID()
})
if (userRepositoryObj?.role in permissions.repository.getRepository) {
// User can fetch/get the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}

updateRepository(
repository,
user
) {
userRepositoryObj = UserRepository.get({
userID: user.getID(),
repositoryID: repository.getID()
})
if (userRepositoryObj?.role in permissions.repository.updateRepository) {
// User can fetch/get the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}

deleteRepository(
repository,
user
) {
userRepositoryObj = UserRepository.get({
userID: user.getID(),
repositoryID: repository.getID()
})
if (userRepositoryObj?.role in permissions.repository.deleteRepository) {
// User can fetch/get the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}

configureRepositorySettings(
repository,
user
) {
userRepositoryObj = UserRepository.get({
userID: user.getID(),
repositoryID: repository.getID()
})
if (userRepositoryObj?.role in permissions.repository.configureRepositorySettings) {
// User can fetch/get the repository
return http.StausOK
}
// The user is not authorized to access the repository
return http.StatusForbidden
}
}

With this implementation, we are bucketing permissions to a role, and assigning a role to each user.

Adding any new features, and asking who is authorized to use this feature becomes simpler, we simply need to ask which role is authorized to perform this action. Adding new users is also much simpler, we simply need to decide the role of the user

However, things will get more complex when we start adding more entities to our application. For example, let’s add two new entities, “Organization” and “Workflows”. Let’s define them more clearly -

  1. Workflows are like GitHub Actions, where users can run a CICD pipeline.
  2. Organizations are a parent entity to Repositories and Workflows. Each repository and workflow will belong to an organization.

For our understanding, let’s visualize them as parent-child relationships -

These entities can have their own admins and members with their own permissions. For example, an organization admin can have the permission to add or remove members. While a workflow admin can create/remove workflows.

To solve this, we would need to adapt our data model like such —

And redefine our permissions -

{
"permissions": {
"repository": {
"getRepository": ["owner", "member"],
"updateRepository": ["owner"],
"deleteRepository": ["owner"],
"configureRepositorySettings": ["owner"]
},
"organization": {
"getOrganization": ["admin", "member", "guest"],
"deleteOrganization": ["admin"],
"updateOrganization": ["admin"]
},
"workflow": {
"runWorkflow": ["admin", "member"],
"createWorkflow": ["admin"],
"deleteWorkflow": ["admin"]
}
}
}

This is still pretty good, our application logic doesn’t decide who can perform what action and all permissions are defined in the declarative JSON file.

If this is all your application requires for authorization, its probably a good idea to stop here.

But as you start building more features, you’ll start to see the flaws in this model pretty quickly. For one, when you create a user, you need to add the user to each repository of the organization after you’ve added him/her to the organization. This seems redundant, it might be more efficient if each user of the organization automatically has access to read all repositories of the organization. Also, admins of the organization should automatically be the admins of repositories and workflows.

Let’s add one more entity to highlight this problem further, “issues”. Any user who has access to read the repository should be able to create and comment on issues in the repository. The creator of the issue should be able to delete the issue.

Issues are a “child” of a repository -

Each repository and workflow is associated with an organization, and each issue is associated with a repository. When you start building it to get production-ready, you’ll soon start to add more and more entities, for example, WorkflowRuns, etc. Our complete tree could look something like this -

Our current RBAC model will require a role for each entity which will get complex really quickly.

Let’s see how to resolve this with ReBAC.

ReBAC

One way to resolve this is to write these entities as relationships. And that’s how we think about it as well. For example, any member of the organization should always be allowed to read repositories of the organization. And an admin should be allowed to create and update repositories.

As we saw, these entities are related to each other, for example, a repository is in an organization, and an issue belongs to a repository.

This requires quite a different way of thinking about these entities. Writing it declaratively isn’t intuitive either. We must completely change our thinking from “attributes” and consider everything as “relations”.

It’s a little difficult to think this way, so let’s build our mental model gradually. Let’s start with the entities,

Now, let’s start modelling the relations. Each relation will be an edge between two entities. So each relation will define two participating entities and a relation name. For example, we can say a user:sanil is a member of repository:golangService. Here the relation is member, and the entities are user:sanil and repository:golangService.

The next step is to define these relationships in a declarative way. Fortunately, there are many pre-existing open-source tools we can use to build this authorization model. I will use OpenFGA, which is a very popular tool(you can play around with it here — https://play.fga.dev/). This is how we can model the entities and their relationships in OpenFGA’s declarative language -

model
schema 1.1

type user

type organization
relations
define admin: [user]
define member: [user]

type repository
relations
define organization: [organization]
define admin: [user]
define member: [user]

type workflow
relations
define organization: [organization]
define admin: [user]
define member: [user]

type issue
relations
define poster: [user]
define repository: [repository]

It might seem confusing at first, but let’s try to walk through it.

Each type represents an entity type, such as user, organization, repository, workflow, and issue. These are the core objects in the system. The relations section allows us to define how these entities are connected to each other. For example, a repository is part of an organization, and both users and admins can be linked to the repository through their roles.

We define relationships between entities with statements like define admin: [user], meaning that a user can be assigned the "admin" role for a given entity, such as an organization or repository.

In OpenFGA, we express relationships using tuples. A tuple is a set of three values: the actor, the relation, and the resource. For example:

(user:sanil, member, repository:golangService)

This tuple means that user:sanil is a member of repository:golangService. Similarly, we can define relationships for admins, organizations, and workflows.

We have modelled the entities, and their relations to each other, but we are yet to define permissions in the system.

Permissions in OpenFGA can also be modelled as relationships. For instance, if a user can only update a repository if they are an admin, we represent this permission by checking the admin relationship on that repository. Here’s how that might look in the model:

type repository
relations
define organization: [organization]
define admin: [user]
define member: [user]
define can_update: admin

Let’s add all the permissions we have discussed so far -

model
schema 1.1

type user

type organization
relations
define admin: [user]
define member: [user]

type repository
relations
define organization: [organization]
define admin: [user]
define member: [user]

define can_read: admin or member or admin from organization or member from organization
define can_write: admin or admin from organization
define can_update: admin or admin from organization
define can_delete: admin or admin from organization
define can_configure: admin or admin from organization

type workflow
relations
define organization: [organization]
define admin: [user]
define member: [user]

define can_read: member or member from organization
define can_write: admin or admin from organization
define can_run: member or member from organization

type issue
relations
define poster: [user]
define repository: [repository]

define can_delete: poster
define can_comment: member from repository

When defining who is allowed a certain permission, for example, who is allowed can_write on a repository, instead of just defining admin, we can also write any admin of the organization of the repository is also added to this permission -

type workflow
relations
define organization: [organization]
define admin: [user]
define member: [user]

define can_read: member or member from organization
define can_write: admin or admin from organization
define can_run: member or member from organization

When we create a repository, we simply need to add a tuple to OpenFGA,

(organization:MyOrg, organization, repository:nodeJSRepository)

The tuple represents that organization:MyOrg is the organization associated with repository:nodeJSRepository. Since we’ve defined in the model that any member of an organization automatically gets can_read permission on all repositories, all members of organization:MyOrg will be able to read the nodeJSRepository.

OpenFGA also allows us to query these relationships to determine permissions dynamically. When querying, we ask an authorization question of the form: “Is user X allowed to perform operation Y on resource Z?”

For example:

Is user:sanil allowed to can_read on repository:nodeJSRepository?

OpenFGA checks the relationships, evaluates the permissions, and returns the result. Since user:sanil is a member of organization:MyOrg, they inherit the can_read permission on the nodeJSRepository as defined in our model.

Usually, this is where your journey into authorization would stop, as ReBAC covers most typical use cases. However, there are scenarios where you might need a more flexible and broader authorization model.

Let’s explore an example use case. While GitHub doesn’t currently implement this, I’ll imagine a new feature to illustrate the point:

Suppose GitHub allowed repositories to be tagged with team names, such as tag:FeatureTeam or tag:PlatformTeam. Similarly, each user would be tagged with their team. Instead of granting access to all repositories in an organization, we want users to only access the repositories that match their team’s tag.

If you try to model this using ReBAC alone, you’ll quickly realize that it’s not possible — or at least not cleanly. This is because ReBAC is focused on relationships between entities and isn’t great when handling tag-based, attribute-driven access like this.

Let’s understand how to model these usecases with ABAC.

ABAC

ABAC is a much broader Authorization model that can cover even the most extreme usecases that RBAC and ReBAC simply don’t cover.

So, how does it work? In ABAC, access control decisions are made by evaluating the attributes of the user, the resource they want to access, and sometimes the environment or context. Essentially, you define rules that compare these attributes and determine whether or not a specific action should be allowed. These attributes could be anything — team names, project tags, user roles, or even contextual factors like time or location.

Let’s take our Github tag example to understand it better. Imagine that each repository in GitHub is tagged with a team name, such as tag:FeatureTeam or tag:PlatformTeam. Similarly, each user in the organization has an attribute that represents their team, like user:Sanil -> team:FeatureTeam. Using ABAC, we can create a rule that says:
"Allow a user to read a repository only if the user’s team attribute matches the repository’s team tag."

In this case, when user:Sanil tries to access repository:ProjectX (which is tagged as tag:FeatureTeam), the system checks the attributes of both the user and the repository. Since John’s team matches the repository’s tag, he’s granted access. If a user from the PlatformTeam tries the same, they would be denied access because their team attribute doesn’t match the repository’s tag.

I’d have loved to cover how to implement ABAC in this post as well(been exploring Casbin for some time) but this post has already gotten way too long.

Conclusion

This was a really fun topic to dive into and explore. Authorization is a really interesting problem statement, and even after spending so much time researching and understanding, there’s still more to be learnt. I’d also highly recommend reading the Google Zanzibar paper —

Zanzibar: Google's Consistent, Global Authorization System

If you enjoyed this topic, you will probably like some other blogs I’ve written as well, for example, when I dived deep into how Golang’s Thread scheduler works, and why is Golang so different from other languages —

Understanding the Go Scheduler and looking at how it works

Or when I read the code for ls and explained why it’s referred to as the most “overengineered” utility package -

How Does `ls` Work?


Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Sanil Khurana


Print Share Comment Cite Upload Translate Updates
APA

Sanil Khurana | Sciencx (2024-09-23T01:09:21+00:00) Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC. Retrieved from https://www.scien.cx/2024/09/23/complete-guide-to-building-authorization-systems-using-rbac-rebac-and-abac/

MLA
" » Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC." Sanil Khurana | Sciencx - Monday September 23, 2024, https://www.scien.cx/2024/09/23/complete-guide-to-building-authorization-systems-using-rbac-rebac-and-abac/
HARVARD
Sanil Khurana | Sciencx Monday September 23, 2024 » Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC., viewed ,<https://www.scien.cx/2024/09/23/complete-guide-to-building-authorization-systems-using-rbac-rebac-and-abac/>
VANCOUVER
Sanil Khurana | Sciencx - » Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/09/23/complete-guide-to-building-authorization-systems-using-rbac-rebac-and-abac/
CHICAGO
" » Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC." Sanil Khurana | Sciencx - Accessed . https://www.scien.cx/2024/09/23/complete-guide-to-building-authorization-systems-using-rbac-rebac-and-abac/
IEEE
" » Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC." Sanil Khurana | Sciencx [Online]. Available: https://www.scien.cx/2024/09/23/complete-guide-to-building-authorization-systems-using-rbac-rebac-and-abac/. [Accessed: ]
rf:citation
» Complete Guide to Building Authorization Systems using RBAC, ReBAC and ABAC | Sanil Khurana | Sciencx | https://www.scien.cx/2024/09/23/complete-guide-to-building-authorization-systems-using-rbac-rebac-and-abac/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.