jakhin03 / Practices for Effective Permissions Management

Created Wed, 20 Nov 2024 00:22:51 +0700 Modified Mon, 09 Dec 2024 11:20:18 +0700
863 Words

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:

  1. It becomes increasingly complex when you add new roles.
  2. 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