RBAC vs ABAC: Understanding Modern Authorization Models
Welcome to Permission Systems That Scale
In this blog, you'll learn how to design robust, maintainable authorization systems that can handle real-world business requirements. By the end, you'll understand the strengths, limitations, and trade-offs of different authorization models, along with how to implement them from scratch.
What You'll Learn
Throughout this workshop, you'll learn how to:
- Implement Role-Based Access Control (RBAC) using clean and type-safe code.
- Build Attribute-Based Access Control (ABAC) for complex and dynamic authorization scenarios.
- Understand how popular authorization libraries like CASL and Casbin manage permissions.
- Organize permission logic in a way that is scalable, maintainable, and easy to extend.
Authentication vs Authorization
Before exploring permission systems like RBAC and ABAC, it's important to understand two fundamental security concepts that are often confused: authentication and authorization.
| Authentication | Authorization |
|---|---|
| Who are you? | What are you allowed to do? |
| Verifies a user's identity | Determines a user's permissions |
| Happens before authorization | Happens after authentication |
| Example: Logging in with an email and password | Example: Checking whether a user can delete a document |
Think of it like entering an office building.
- Authentication is showing your employee ID at the entrance to prove your identity.
- Authorization is determining which floors or rooms your ID card allows you to access.
Why Authorization Is More Challenging
Authentication is usually handled by authentication providers such as Auth.js, Clerk, Firebase Authentication, or Supabase Auth. Authorization, however, is unique to every application because it depends entirely on business requirements.
1. Every Application Has Different Business Rules
Permission rules are rarely the same between applications.
For example:
- Editors can update documents only within their own department.
- Authors can publish their own drafts but cannot publish someone else's content.
- Administrators can manage users but cannot delete archived records.
Unlike authentication, these rules cannot simply be copied from another project. They must be designed specifically for your application's business logic.
2. Authorization Exists Throughout the Application
Permission checks are not limited to a single location. They appear across the entire application.
| Layer | Example Permission Check |
|---|---|
| API | Can this user call this endpoint? |
| Database | Which records should this user be allowed to query? |
| UI | Should this button or menu item be visible? |
| Business Logic | Is this operation allowed for the current user? |
As an application grows, authorization becomes one of the most frequently used parts of the codebase.
3. Business Requirements Continuously Change
Authorization rules rarely stay the same.
Over time, your application may require changes such as:
- Introducing new user roles.
- Restricting access after a certain period.
- Allowing managers to approve requests across departments.
- Supporting multiple organizations or tenants.
A poorly designed permission system quickly becomes difficult to maintain and slows down future development.
Common Problems Caused by Poor Authorization
An ineffective authorization system can create serious security and usability issues.
| Problem | Result |
|---|---|
| Overly permissive access | Sensitive data may be exposed or modified by unauthorized users. |
| Overly restrictive access | Legitimate users cannot perform actions they should be allowed to do. |
| Inconsistent permission checks | Some endpoints remain protected while others accidentally expose data. |
| Scattered permission logic | Every feature becomes harder to develop, test, and maintain. |
The Core Question of Authorization
Once a user has successfully logged in, authentication is complete.
The next question becomes:
Given this authenticated user, what actions should they be allowed to perform?
A permission system exists to answer that question consistently and securely.
Characteristics of a Good Permission System
A well-designed authorization system should follow several important principles.
| Principle | Description |
|---|---|
| Protect all sensitive data | Every request should be validated before data is read or modified. |
| Single Source of Truth | Permission rules should be centralized instead of scattered throughout the codebase. |
| Automatic Enforcement | Authorization should be applied automatically rather than relying on developers to remember adding permission checks. |
| Consistent Behavior | The same permission rules should produce identical results on both the frontend and backend. |
| Never Trust the Client | Client-side permission checks improve user experience only. All security decisions must always be enforced on the server. |
| Fail Closed | If a permission cannot be verified, access should be denied by default instead of being granted. |
Best Practice
Client-side authorization is only for improving the user experience, such as hiding buttons or disabling actions. The server must always perform the final authorization check before processing a request.
Understanding the Data Model
Before implementing any permission system, it's important to understand what we're protecting.
In this example application, there are three main entities:
- Users – People who use the application.
- Projects – Containers that organize documents.
- Documents – The actual content users create and manage.
Each entity has its own responsibilities and relationships.
| Entity | Purpose |
|---|---|
| User | Represents an authenticated person with a specific role and department. |
| Project | Groups related documents together and can belong to a department. |
| Document | Stores the actual content created and edited by users. |
The permission system decides which users can perform actions such as viewing, creating, editing, or deleting these resources.
The First Problem I Noticed
When I first looked at the application, the permission system worked, but the authorization logic was spread across different parts of the codebase.
For example, permission checks existed inside:
- Page components
- API actions
- Database access logic
- UI components
This made the code difficult to maintain because the same permission rules were written multiple times.
Why Is This a Problem?
When authorization logic is duplicated, it creates several issues.
| Problem | Impact |
|---|---|
| Duplicate permission checks | The same rule has to be updated in multiple places. |
| Missing authorization | Some pages or APIs may accidentally skip security checks. |
| Inconsistent logic | Different developers may implement the same permission differently. |
Imagine updating a rule so that Managers can edit projects. If the permission check exists in ten different files, forgetting to update just one of them can introduce security bugs.
A Better Approach
Instead of checking permissions everywhere, it's much cleaner to centralize authorization in a dedicated service layer.
Pages
│
▼
Services (Authorization + Business Logic)
│
▼
Database
With this approach:
- Pages focus on rendering the UI.
- Services handle authorization and business rules.
- The data layer is responsible only for database operations.
This keeps the application easier to understand, test, and maintain.
Key Takeaways
- Keep authorization logic in one place.
- Never trust client-side permission checks.
- Always validate permissions on the server.
- Use descriptive helper functions instead of repeating role comparisons.
- Design permission systems that can evolve as business requirements change.
These principles make permission systems easier to maintain while reducing the chances of introducing security vulnerabilities.
What is Role-Based Access Control (RBAC)?
Role-Based Access Control (RBAC) is one of the most common authorization models used in modern applications.
The idea behind RBAC is simple:
Instead of assigning permissions directly to users, permissions are assigned to roles, and users are assigned those roles.
User → Role → Permissions
For example, instead of checking whether a user is an administrator throughout your application, you define the administrator's permissions once and reuse them everywhere.
| Role | Common Permissions |
|---|---|
| Viewer | Read data |
| Author | Create and edit their own content |
| Editor | Manage and update content |
| Admin | Full access |
Why RBAC Is Better Than Inline Permission Checks
When I first started learning authorization, I noticed that many applications directly compare user roles throughout the codebase.
if (user.role === "admin") {
// Allow access
}
At first, this seems simple, but as the application grows, the same condition gets repeated in many different files.
A better approach is to centralize permissions.
// Define your permissions
type Permission =
| "document:create"
| "document:read"
| "document:update"
| "document:delete"
| "project:create"
| "project:read"
| "project:update"
| "project:delete"
// Map roles to permissions
const permissionsByRole: Record<UserRole, Permission[]> = {
admin: [
"document:create",
"document:read",
"document:update",
"document:delete",
"project:create",
"project:read",
"project:update",
"project:delete",
],
editor: ["document:read", "document:update"],
viewer: ["document:read"],
}
// Check if a user can perform an action
function can(user: { role: UserRole }, permission: Permission) {
return permissionsByRole[user.role].includes(permission)
}
Usage is clean and readable:
if (can(user, "document:create")) {
// Show create button
}
if (!can(user, "project:delete")) {
throw new AuthorizationError()
}
if (can(user, "document:create")) {
// Allow access
}
This small change makes the code much easier to read and maintain.
Benefits of RBAC
Some of the biggest advantages of RBAC include:
- Centralized permission management
- Cleaner and more readable code
- Easier maintenance
- Reduced duplicate permission checks
- Better type safety when using TypeScript
Instead of searching the entire project for role comparisons, permission changes only need to be updated in one place.
Where RBAC Starts to Struggle
RBAC works really well when permissions depend only on a user's role.
However, real-world applications often require additional conditions.
For example:
- Only the creator can edit their own draft.
- Locked documents cannot be edited.
- Users can only access resources in their own department.
- Access depends on the current time or location.
These rules depend on more than just the user's role.
For example:
User is Author
+
Owns the Document
+
Document is Draft
+
Document is Not Locked
↓
Can Edit
Trying to represent these rules using only roles quickly becomes difficult.
Permission Explosion
As business requirements grow, RBAC permissions often become more specific.
Instead of simple permissions like:
document:read
document:update
they slowly evolve into:
document:update:own
document:update:own:draft
document:update:own:draft:unlocked
This is commonly known as permission explosion.
Managing dozens of highly specific permissions makes the system harder to understand and maintain.
My Takeaway
RBAC is an excellent choice for applications with relatively simple authorization requirements.
If permissions mainly depend on a user's role, RBAC provides a clean and maintainable solution.
However, once permissions depend on ownership, resource attributes, departments, document status, or other contextual information, RBAC starts becoming difficult to scale.
What is Attribute-Based Access Control (ABAC)?
While learning about permission systems, I discovered that Role-Based Access Control (RBAC) isn't always enough for real-world applications.
Sometimes, permissions depend on much more than a user's role.
For example:
- Is the user the creator of the document?
- Is the document still a draft?
- Is the document locked?
- Does the user belong to the same department?
These types of rules are difficult to express using only roles. This is where Attribute-Based Access Control (ABAC) becomes useful.
How ABAC Works
Instead of asking:
"Does this role have permission?"
ABAC asks:
"Do the attributes of this request satisfy the access policy?"
An authorization decision is made by evaluating different attributes.
| Attribute | Examples |
|---|---|
| User (Subject) | Role, ID, Department |
| Resource | Owner, Status, Locked State |
| Action | Read, Create, Update, Delete |
| Environment | Time, Location, Device |
RBAC vs ABAC
| RBAC | ABAC |
|---|---|
| Permissions are based on user roles. | Permissions are based on multiple attributes. |
| Simple to implement. | More flexible and scalable. |
| Best for straightforward permission models. | Best for applications with complex business rules. |
A Practical Example
Suppose an author should only be able to edit their own draft documents.
With RBAC, this quickly becomes difficult because the decision depends on multiple conditions.
ABAC can express the same rule naturally:
User Role = Author
+
Document Owner = Current User
+
Document Status = Draft
+
Document Locked = No
↓
Allow Edit
Instead of creating dozens of specialized permissions, ABAC simply evaluates the required attributes.
Why ABAC Scales Better
As an application grows, business rules become more complex.
For example:
- Managers can only access projects in their department.
- Employees can edit documents only during business hours.
- Confidential documents require a higher security clearance.
- Only the creator can modify an unpublished document.
These requirements are difficult to model with RBAC alone, but they fit naturally into ABAC because authorization is based on policies rather than predefined roles.
My Takeaway
RBAC is an excellent choice when permissions depend mainly on user roles.
However, once authorization starts depending on ownership, document state, departments, or other contextual information, ABAC provides a much cleaner and more scalable solution.
Choosing between RBAC and ABAC isn't about which one is better—it's about selecting the right model for your application's complexity.
Taking ABAC One Step Further
One thing I found interesting while learning ABAC is that it isn't limited to deciding whether a user can perform an action. It can also control what data users can see, what they can modify, and even when they are allowed to perform an action.
This makes ABAC much more powerful than traditional RBAC.
Field-Level Permissions
Sometimes users should have access to a resource but not every field inside it.
For example, every user may be able to view a document, but only administrators should see certain metadata.
| Field | Admin | Editor | Author | Viewer |
|---|---|---|---|---|
| Title | ✅ | ✅ | ✅ | ✅ |
| Content | ✅ | ✅ | ✅ | ✅ |
| Created At | ✅ | ✅ | ✅ | ❌ |
| Updated At | ✅ | ✅ | ✅ | ❌ |
Similarly, write permissions can also be restricted.
For example, an author may update the document content but shouldn't be able to publish or lock the document.
This level of control is difficult to achieve with RBAC alone.
Environment-Based Permissions
Another feature I learned about is that authorization doesn't always depend on the user or the resource.
Sometimes the environment also matters.
Examples include:
- Allow editing only during business hours.
- Restrict access based on location.
- Allow specific actions only from trusted devices.
- Block updates during weekends.
These rules make permissions more context-aware and are one of the biggest strengths of ABAC.
Automatic Data Filtering
A good permission system shouldn't only decide whether a user has access—it should also ensure that users only receive data they are allowed to see.
Instead of fetching every record and filtering it manually, authorization rules can be applied directly to database queries.
This approach provides several benefits:
- Improved security
- Better performance
- Less duplicated code
- Lower risk of accidentally exposing sensitive data
Is ABAC Always the Right Choice?
Although ABAC is incredibly flexible, it also introduces additional complexity.
As more resources, attributes, and policies are added, the authorization logic becomes harder to understand and maintain.
For smaller applications, a simple RBAC implementation is often enough.
For larger applications with ownership rules, departments, document states, or contextual permissions, ABAC usually provides a cleaner and more scalable solution.
My Takeaway
The biggest lesson I learned is that there isn't a single permission model that fits every application.
- RBAC is simple, easy to implement, and works well for straightforward role-based permissions.
- ABAC offers much greater flexibility by making authorization decisions based on attributes instead of roles.
Choosing the right approach depends on your application's complexity rather than simply picking the more advanced solution.
Using CASL Instead of Building Everything Yourself
While building a custom ABAC system is a great way to understand how authorization works, maintaining it in a production application can become challenging.
Instead of implementing every feature yourself, you can use a mature authorization library like CASL.
CASL provides many advanced features out of the box, allowing you to focus on your application's business logic instead of maintaining a custom permission engine.
Why Use CASL?
After exploring CASL, I found several advantages over a custom implementation.
- Built-in support for RBAC and ABAC
- Field-level permissions
- Advanced conditional rules
- TypeScript support
- Well-tested and widely adopted by the community
Because these features are already implemented, you don't have to reinvent the wheel.
A Simple Example
Creating permissions with CASL is straightforward.
import { AbilityBuilder, createMongoAbility } from "@casl/ability";
const { can, build } = new AbilityBuilder(createMongoAbility);
can("read", "Document");
can("update", "Document", { creatorId: user.id });
const ability = build();
Checking permissions is equally simple.
if (ability.can("update", subject("Document", document))) {
// User is allowed to update the document
}
Things to Consider
Although CASL is a powerful library, it isn't always the right choice.
Some trade-offs include:
- A slightly steeper learning curve.
- Extra configuration compared to a simple RBAC implementation.
- Additional concepts such as
subject()when working with plain JavaScript objects.
For small projects, these trade-offs may not be worth it.
When Should You Use CASL?
| Choose CASL When | Avoid CASL When |
|---|---|
| Your application has complex authorization rules. | Your project only has a few roles and simple permissions. |
| You need field-level or attribute-based permissions. | A basic RBAC implementation is sufficient. |
| You don't want to maintain a custom permission engine. | You prefer keeping dependencies to a minimum. |
My Takeaway
One of the biggest lessons I learned is that you don't always need to build your own authorization system.
If your application's permission requirements are relatively simple, a custom RBAC or lightweight ABAC implementation can work well.
However, for large applications with complex authorization rules, using a battle-tested library like CASL can save time, reduce maintenance, and improve reliability.
The important thing isn't choosing the most advanced solution—it's choosing the one that best fits your application's requirements.
Resources
If you'd like to explore authorization and permission systems in more depth, I highly recommend the following resource:
- Permission Systems That Scale by Web Dev Simplified
https://webdevsimplified.github.io/fem-permission-systems-that-scale/