This post delves into the evolution of access control strategies. Whether you are a developer struggling with intricate access controls or a system architect planning a robust authentication framework, this guide offers insights into transforming permission handling from a potential bottleneck to a strategic advantage.
The problem with traditional permission checking
const user = {role: "admin", id: "1"}
if user[role] = "admin":
doSomething()
This approach has two major pitfalls:
- It becomes increasingly complex when you add new roles.
- Multiple if-else statements make the code hard to maintain and extend.
Role-based access control (RBAC)
RBAC introduces a more elegant solution by defining permissions based on roles. The core idea is to break down what someone can do into two key components:
- Permissions (view, create, update, delete).
- Resources (comments, products, blogs, articles, etc.).
Here is a TypeScript implementation that demonstrates this concept:
type Role = keyof typeof ROLES
type Permission = (typeof ROLES)[Role] [number]
const ROLES={
admin: [
"view: comments",
"create:comments",
update:comments",
"delete:comments",
],
moderator: [
"view: comments",
"create: comments",
"delete: comments"
]
user: [
"view: comments"
"create: comments"
]
}
export function hasPermission(
user: {id: string; role: Role},
permission: Permission
){
return (ROLES[user.role] as readonly Permission[]).include(permission)
}
RBAC limitation
While RBAC provides a structured approach, It starts to break down when:
- You need role-specific attributes.
- Your system becomes more complex.
- You want fine-grained, context-aware permissions.
user:{'update: ownComments', 'create: ownComments', 'delete: ownComments'}
Database Diagrams
Permission systems don’t emerge fully formed - they evolve through increasingly complex stages. Let’s explore the architectural progression:
1. Simple User-Role
The most basic model: a direct connection between users and roles.
- Pros: Simple to implement.
- Cons: Extremely limited flexibility.
- Use case: Small applications with minimal access control needs. ![[SimplePermissionDiagram.svg]]
2. Multiple Roles per User
As systems grow more complex, the need for nuanced access becomes apparent
- Key Improvement: Allows a single user to have multiple roles.
- Flexibility Gained:
- A user could be both a “content creator” and a “project manager”.
- Enables more granular access control.
- Design Consideration: Even if you initially assign only one role per user, having the capability provides future-proofing. ![[MultipleRolePerUserDiagram.svg]]
3. User-Role Permission Model
Introduces explicit permission mappings to roles.
- Components:
- User table.
- Role table.
- Explicit permission definitions.
- Benefits:
- Clear separation of concerns.
- More predictable access control.
- Easier to audit and manage. ![[UserRolePermission.svg]]
4. Organization-level permissions
Critical for team-based and multi-tenant applications
- Real-world examples: Slack, Google Workspace.
- Key features:
- Users can belong to multiple organizations.
- Different roles and permissions per organization.
- Ability to invite/manage team members. ![[UserRolePermissionUpperLevel.svg]]
5. Advanced sharing permissions
Inspired by systems like Google Drive.
- Complex Scenarios:
- Share resources across different systems.
- Granular access levels (view, comment, edit).
- Cross-resource permission management.
- Implementation strategy:
- User generic tables.
- Add type indicators (
type=="blogs"
,type=="files"
, etc.). - Create flexible sharing mechanisms ![[UserRolePermissionUpperLevelBlogRole.svg]]
[!NOTE] Conclusion This is rather a complicated rule based system. We need to implement this in an easier way instead of having the use of these full on table systems
Attribute-based access Control (ABAC)
ABAC represents the most sophisticated approach to permission management, focusing on contextual decision-making.
Core components of ABAC
1. Subject (Who)
- The entity attempting an action.
- Not just limited to user roles.
- Can include additional attributes like: user department, employment status, location, time of access
2. Action (What)
- The specific operation being attempted.
- Examples: read, create, update, delete, share
3. Resource (Which)
- The specific object being acted upon.
- Can be: documents, files, comments, database records
4. Context (When and Where)
- Environmental and contextual factors.
- Example: time, network location, device type, organizational unit, etc.
ABAC Implementation:
interface AccessRequest {
subject: {
id: string;
role: string;
department: string;
employmentType: 'full-time' | 'part-time' | 'contractor';
};
action: 'read' | 'write' | 'delete';
resource: {
type: string;
owner: string;
sensitivity: 'low' | 'medium' | 'high';
};
context: {
time: Date;
location: string;
deviceType: string;
};
}
function checkAccess(request: AccessRequest): boolean {
// Complex, context-aware permission logic
const rules = [
// Full-time employees can read high-sensitivity documents in their department
request.subject.employmentType === 'full-time' &&
request.subject.department === request.resource.owner &&
request.action === 'read' &&
request.resource.sensitivity === 'high',
// Contractors have limited access during work hours
request.subject.employmentType === 'contractor' &&
request.context.time.getHours() >= 9 &&
request.context.time.getHours() <= 17 &&
request.action !== 'delete'
];
return rules.some(rule => rule === true);
}
Implementing RBAC and ABAC with Clerk
Clerk provides a powerful, developer-friendly approach to managing authentication and authorization
User creation and role assignment
import { clerkClient } from '@clerk/nextjs/server';
async function handleUserCreation(event) {
// Automatic role assignment based on registration context
const defaultRole = determineInitialRole(event.data);
await clerkClient().users.updateUser(event.data.id, {
publicMetadata: {
role: defaultRole,
// Additional ABAC attributes
department: event.data.department,
employmentType: event.data.employmentType
}
});
}
function determineInitialRole(userData) {
// Complex role assignment logic
if (userData.email.endsWith('@company.com')) {
return userData.department === 'engineering' ? 'developer' : 'employee';
}
return 'user';
}
Permission checking middleware
function checkPermission(user, action, resource) {
// Combine RBAC and ABAC strategies
const rolePermissions = ROLES[user.publicMetadata.role];
const contextPermissions = evaluateContextualRules(user, action, resource);
return rolePermissions.includes(action) && contextPermissions;
}
Conclusion
Permission handling isn’t just about restricting access - It’s about creating a flexible, maintainable system that can grow with your applications needs. From basic RBAC to advanced ABAC, the key is to think strategically about how permission will work today and in the future.
Reference
Inspirational Content: https://youtu.be/5GG-VUvruzE