Skip to content

Instantly share code, notes, and snippets.

@amal-babu-git
Last active July 15, 2025 09:05
Show Gist options
  • Select an option

  • Save amal-babu-git/87aa40d13ef55a6313ea7000dd9cca2d to your computer and use it in GitHub Desktop.

Select an option

Save amal-babu-git/87aa40d13ef55a6313ea7000dd9cca2d to your computer and use it in GitHub Desktop.

ERP Multi-Tenant Permission System - Comprehensive Documentation

Table of Contents

  1. Overview & Architecture
  2. Theoretical Concepts
  3. User Types & Hierarchies
  4. Permission System Components
  5. Role-Based Access Control (RBAC)
  6. Permission Classes & Implementation
  7. Using Custom Permissions in ViewSets
  8. Code Examples & Implementation
  9. API Security & Access Control
  10. Advanced Permission Patterns
  11. Testing & Debugging
  12. Best Practices & Performance

Overview & Architecture

This ERP application implements a sophisticated multi-tenant permission system with role-based access control (RBAC) designed for SaaS applications. The system ensures proper data isolation between tenants while providing granular permission control within each tenant.

Key Features

  • Multi-tenant Architecture: Complete data isolation between tenants
  • Role-Based Access Control: Users have roles that contain permissions
  • Granular Permissions: Module-based permissions with CRUD operations
  • Hierarchical Access: Support for manager-subordinate relationships
  • Permission Caching: Optimized performance with intelligent caching
  • Template-Based Roles: Predefined permission sets for common roles

Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│                    MULTI-TENANT PERMISSION SYSTEM               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  │   Super Admin   │    │  Tenant Admin   │    │    Employee     │
│  │  (Global Access)│    │ (Tenant Access) │    │ (Branch Access) │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘
│           │                       │                       │
│           │                       │                       │
│  ┌─────────────────────────────────────────────────────────────┐
│  │                 PERMISSION LAYERS                           │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │
│  │  │ Base        │  │ Tenant      │  │ Role-Based  │       │
│  │  │ Permission  │  │ Permission  │  │ Permission  │       │
│  │  └─────────────┘  └─────────────┘  └─────────────┘       │
│  └─────────────────────────────────────────────────────────────┘
│                                                                 │
│  ┌─────────────────────────────────────────────────────────────┐
│  │                     RBAC COMPONENTS                         │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │
│  │  │ Permissions │  │    Roles    │  │ Templates   │       │
│  │  │ (Atomic)    │  │ (Groups)    │  │ (Presets)   │       │
│  │  └─────────────┘  └─────────────┘  └─────────────┘       │
│  └─────────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────┘

Theoretical Concepts

1. Multi-Tenancy & Data Isolation

The system implements schema-based multi-tenancy where each tenant has:

  • Separate database schema for tenant-specific data
  • Shared public schema for cross-tenant user management
  • Isolated permission contexts per tenant

2. Role-Based Access Control (RBAC)

RBAC follows the principle of least privilege:

  • Users are assigned Roles
  • Roles contain Permissions
  • Permissions define Actions on Modules

3. Permission Hierarchy

Module → Permission Type → Action
   │           │             │
   │           │             └─ read, write, delete, execute
   │           │
   │           └─ Granular control per operation
   │
   └─ System modules (hr, finance, employees, etc.)

4. Access Control Layers

  1. Authentication: User must be logged in
  2. Tenant Isolation: User can only access their tenant's data
  3. Role Verification: User must have required role
  4. Permission Check: Role must have specific permission
  5. Object-Level Access: Additional checks for specific objects

User Types & Hierarchies

1. Super Admin (super_admin)

Scope: Global platform administration

# Properties
user.is_super_admin = True
user.tenant = None  # No tenant association
user.is_superuser = True

# Capabilities
- Access ALL tenants and data
- Create/suspend/manage tenants
- Platform-wide system administration
- Bypass all permission checks

Database Storage: Public schema only

2. Tenant Admin (tenant_admin)

Scope: Full control within specific tenant

# Properties
user.is_tenant_admin = True
user.tenant = <specific_tenant>
user.is_superuser = False

# Capabilities
- Manage all employees in tenant
- Create/modify branches
- Assign roles and permissions
- Access all modules within tenant

Database Storage:

  • User record: Public schema
  • Profile: ClientAdminProfile (Public schema)

3. Employee (employee)

Scope: Limited access based on role and branch

# Properties
user.is_employee = True
user.tenant = <specific_tenant>
user.is_superuser = False

# Capabilities
- Access data within assigned branch
- Use modules based on assigned roles
- View/edit own profile
- Hierarchical access to subordinates (if manager)

Database Storage:

  • User record: Public schema
  • Metadata: EmployeeMeta (Public schema)
  • Profile: EmployeeProfile (Tenant schema)

Permission System Components

1. Core Models

Permission Model

class Permission(UUIDModel, TimestampedModel):
    """Individual permission for specific module action"""
    
    PERMISSION_TYPES = [
        ('read', 'Read'),
        ('write', 'Write'),
        ('delete', 'Delete'),
        ('execute', 'Execute'),
    ]
    
    MODULES = [
        ('branches', 'Branches'),
        ('employees', 'Employees'),
        ('hr', 'Human Resources'),
        ('finance', 'Finance'),
        ('inventory', 'Inventory'),
        # ... more modules
    ]
    
    name = models.CharField(max_length=100, unique=True)
    codename = models.CharField(max_length=100, unique=True)
    module = models.CharField(max_length=50, choices=MODULES)
    permission_type = models.CharField(max_length=20, choices=PERMISSION_TYPES)
    description = models.TextField(blank=True)
    is_active = models.BooleanField(default=True)

Role Model

class Role(TimestampedModel):
    """Collection of permissions that can be assigned to users"""
    
    name = models.CharField(max_length=50, unique=True)
    permissions = models.ManyToManyField(Permission, blank=True, related_name='roles')
    permissions_json = models.JSONField(default=dict)  # Cached for performance
    is_active = models.BooleanField(default=True)
    is_system_role = models.BooleanField(default=False)
    
    def has_permission(self, module, permission_type):
        """Check if role has specific permission"""
        return self.permissions.filter(
            module=module,
            permission_type=permission_type,
            is_active=True
        ).exists()

PermissionTemplate Model

class PermissionTemplate(UUIDModel, TimestampedModel):
    """Predefined permission sets for common roles"""
    
    TEMPLATE_TYPES = [
        ('basic_employee', 'Basic Employee'),
        ('manager', 'Manager'),
        ('hr_staff', 'HR Staff'),
        ('finance_staff', 'Finance Staff'),
        # ... more templates
    ]
    
    name = models.CharField(max_length=100, unique=True)
    template_type = models.CharField(max_length=50, choices=TEMPLATE_TYPES)
    permissions = models.ManyToManyField(Permission, blank=True)
    is_active = models.BooleanField(default=True)

2. Employee Profile Integration

class EmployeeProfile(UUIDModel, TimestampedModel):
    """Employee profile with role-based permissions"""
    
    user = models.OneToOneField(User, related_name='tenant_employee_profile')
    branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
    roles = models.ManyToManyField(Role, blank=True)
    
    def has_permission(self, module, permission_type):
        """Check if employee has specific permission"""
        # Tenant admins have all permissions
        if self.user.is_tenant_admin:
            return True
            
        # Check all assigned roles
        for role in self.roles.filter(is_active=True):
            if role.has_permission(module, permission_type):
                return True
        return False
    
    def has_module_access(self, module):
        """Check if employee has any access to a module"""
        if self.user.is_tenant_admin:
            return True
            
        for role in self.roles.filter(is_active=True):
            if role.has_module_access(module):
                return True
        return False

Role-Based Access Control (RBAC)

1. Permission Structure

Each permission follows the pattern: {module}.{action}

# Examples of permission codenames
permissions = [
    "employees.read",      # View employee data
    "employees.write",     # Create/update employees
    "employees.delete",    # Delete employees
    "hr.read",            # View HR data
    "hr.write",           # Manage HR data
    "finance.approve",    # Approve financial transactions
    "branches.manage",    # Manage branches
]

2. Role Creation & Management

# Create a role
role = Role.objects.create(
    name="HR Manager",
    is_system_role=True
)

# Add permissions to role
hr_permissions = Permission.objects.filter(
    module__in=['hr', 'employees'],
    permission_type__in=['read', 'write']
)
role.permissions.set(hr_permissions)

# Assign role to employee
employee.roles.add(role)

3. Permission Templates

# Create a permission template
template = PermissionTemplate.objects.create(
    name="Basic Employee",
    template_type="basic_employee",
    description="Basic permissions for regular employees"
)

# Add permissions to template
basic_permissions = Permission.objects.filter(
    module='employees',
    permission_type='read'
)
template.permissions.set(basic_permissions)

# Apply template to role
template.apply_to_role(role)

Permission Classes & Implementation

1. Core Permission Classes

BasePermission

class BasePermission(permissions.BasePermission):
    """Base class with caching and logging"""
    
    def __init__(self):
        self.cache_timeout = 300  # 5 minutes
    
    def _get_cache_key(self, user_id: str, permission_type: str, context: str = "") -> str:
        return f"perm_{permission_type}_{user_id}_{context}"
    
    def _log_permission_violation(self, request, reason: str, **extra_data):
        logger.warning(f"PERMISSION_VIOLATION: {reason}", extra={
            'user_id': request.user.id,
            'user_email': request.user.email,
            'path': request.path,
            'method': request.method,
            **extra_data
        })

SuperAdminPermission

class SuperAdminPermission(BasePermission):
    """Allows only super admin users"""
    
    def has_permission(self, request, view) -> bool:
        if not request.user.is_authenticated:
            return False
            
        # Cache super admin status
        cache_key = self._get_cache_key(str(request.user.id), "super_admin")
        is_super_admin = cache.get(cache_key)
        
        if is_super_admin is None:
            is_super_admin = request.user.is_super_admin
            cache.set(cache_key, is_super_admin, self.cache_timeout)
            
        return is_super_admin

TenantPermission

class TenantPermission(BasePermission):
    """Ensures users can only access their tenant data"""
    
    def has_permission(self, request, view) -> bool:
        if not request.user.is_authenticated:
            return False
            
        # Super admins can access everything
        if request.user.is_super_admin:
            return True
            
        # Validate tenant access
        current_tenant = getattr(request, 'tenant', None)
        user_tenant = getattr(request.user, 'tenant', None)
        
        if not user_tenant:
            return False
            
        return current_tenant and user_tenant == current_tenant

2. Role-Based Permission Classes

RoleBasedPermission

class RoleBasedPermission(EmployeePermission):
    """Base class for role-based permission checking"""
    
    module = None          # Override in subclasses
    permission_type = None # Override in subclasses
    
    def has_permission(self, request, view):
        # First check basic employee permission
        if not super().has_permission(request, view):
            return False
            
        user = request.user
        
        # Tenant admins have all permissions
        if user.is_tenant_admin:
            return True
            
        # Get employee profile
        try:
            employee_profile = user.tenant_employee_profile
        except AttributeError:
            return False
            
        # Cache the permission check
        cache_key = f"role_perm_{user.id}_{self.module}_{self.permission_type}"
        has_perm = cache.get(cache_key)
        
        if has_perm is None:
            has_perm = employee_profile.has_permission(self.module, self.permission_type)
            cache.set(cache_key, has_perm, 300)
            
        return has_perm

Specific Permission Classes

class EmployeeReadPermission(RoleBasedPermission):
    """Permission for reading employee data"""
    module = 'employees'
    permission_type = 'read'

class EmployeeWritePermission(RoleBasedPermission):
    """Permission for writing employee data"""
    module = 'employees'
    permission_type = 'write'

class HRModulePermission(ModulePermission):
    """Permission for accessing HR module"""
    module = 'hr'

3. Advanced Permission Classes

DynamicModulePermission

class DynamicModulePermission(RoleBasedActionPermission):
    """Determines module from viewset attributes"""
    
    def get_module(self, view):
        # Check if viewset defines permission_module
        if hasattr(view, 'permission_module'):
            return view.permission_module
            
        # Infer from model
        if hasattr(view, 'queryset'):
            model = view.queryset.model
            return model._meta.verbose_name_plural.lower()
            
        return self.module

HierarchyBasedPermission

class HierarchyBasedPermission(RoleBasedPermission):
    """Allows access to subordinate data"""
    
    def has_object_permission(self, request, view, obj):
        if not super().has_object_permission(request, view, obj):
            return False
            
        employee_profile = request.user.tenant_employee_profile
        
        # Always allow access to own data
        if hasattr(obj, 'user') and obj.user == request.user:
            return True
            
        # Check if accessing subordinate data
        if hasattr(obj, 'employee'):
            target_employee = obj.employee
            if target_employee in employee_profile.get_all_subordinates():
                return True
                
        return False

Using Custom Permissions in ViewSets

1. Basic Usage

from rest_framework import viewsets
from .permissions import EmployeeReadPermission, EmployeeWritePermission

class EmployeeViewSet(viewsets.ModelViewSet):
    queryset = EmployeeProfile.objects.all()
    serializer_class = EmployeeSerializer
    
    def get_permissions(self):
        """Dynamic permissions based on action"""
        if self.action in ['create', 'update', 'partial_update']:
            permission_classes = [EmployeeWritePermission]
        elif self.action == 'destroy':
            permission_classes = [EmployeeDeletePermission]
        else:
            permission_classes = [EmployeeReadPermission]
            
        return [permission() for permission in permission_classes]

2. Using Permission Decorators

from client.decorators import require_permission, require_module_access

class HRViewSet(viewsets.ModelViewSet):
    queryset = HRRecord.objects.all()
    serializer_class = HRRecordSerializer
    
    @require_permission('hr', 'read')
    def list(self, request):
        """Only users with hr.read permission can list"""
        return super().list(request)
    
    @require_permission('hr', 'write')
    def create(self, request):
        """Only users with hr.write permission can create"""
        return super().create(request)
    
    @require_module_access('hr')
    def dashboard(self, request):
        """Users with any HR permission can access dashboard"""
        # Custom dashboard logic
        pass

3. Dynamic Permission ViewSet

class DynamicPermissionViewSet(viewsets.ModelViewSet):
    permission_classes = [DynamicModulePermission]
    permission_module = 'finance'  # Define module for permissions
    
    def get_permissions(self):
        """Permissions automatically determined by action"""
        return [permission() for permission in self.permission_classes]

4. Multiple Permission Classes

class SecureViewSet(viewsets.ModelViewSet):
    def get_permissions(self):
        """Multiple permission requirements"""
        if self.action == 'sensitive_action':
            permission_classes = [
                IsAuthenticated,
                TenantPermission,
                EmployeeWritePermission,
                HierarchyBasedPermission
            ]
        else:
            permission_classes = [IsAuthenticated, TenantPermission]
            
        return [permission() for permission in permission_classes]

5. Custom Permission Logic

class CustomBusinessLogicPermission(BasePermission):
    """Custom permission with business logic"""
    
    def has_permission(self, request, view):
        # Basic checks
        if not request.user.is_authenticated:
            return False
            
        # Custom business logic
        if request.method == 'POST':
            # Check if user can create in current context
            return self.can_create_in_context(request)
        
        return True
    
    def has_object_permission(self, request, view, obj):
        # Object-specific logic
        if hasattr(obj, 'created_by'):
            return obj.created_by == request.user
        
        return True
    
    def can_create_in_context(self, request):
        """Custom logic for creation permissions"""
        # Your business logic here
        return True

Code Examples & Implementation

1. Setting Up Permissions

Initialize Default Permissions

# management/commands/setup_permissions.py
from django.core.management.base import BaseCommand
from client.models import Permission, Role, PermissionTemplate

class Command(BaseCommand):
    def handle(self, *args, **options):
        # Create basic permissions
        permissions = [
            ('employees.read', 'View Employees', 'employees', 'read'),
            ('employees.write', 'Manage Employees', 'employees', 'write'),
            ('hr.read', 'View HR Data', 'hr', 'read'),
            ('hr.write', 'Manage HR Data', 'hr', 'write'),
            ('finance.read', 'View Finance Data', 'finance', 'read'),
            ('finance.approve', 'Approve Transactions', 'finance', 'execute'),
        ]
        
        for codename, name, module, perm_type in permissions:
            Permission.objects.get_or_create(
                codename=codename,
                defaults={
                    'name': name,
                    'module': module,
                    'permission_type': perm_type,
                    'is_active': True
                }
            )
        
        # Create role templates
        self.create_role_templates()
        
        # Create default roles
        self.create_default_roles()

Create Employee with Roles

@transaction.atomic
def create_employee_with_roles(user_data, employee_data, role_names):
    """Create employee and assign roles"""
    
    # Create user
    user = User.objects.create_user(
        email=user_data['email'],
        password=user_data['password'],
        first_name=user_data['first_name'],
        last_name=user_data['last_name'],
        user_type='employee',
        tenant=user_data['tenant']
    )
    
    # Create employee profile
    employee = EmployeeProfile.objects.create(
        user=user,
        branch=employee_data['branch'],
        department=employee_data['department'],
        position=employee_data['position'],
        hire_date=employee_data['hire_date']
    )
    
    # Assign roles
    roles = Role.objects.filter(name__in=role_names, is_active=True)
    employee.roles.set(roles)
    
    return employee

2. Permission Checking

Manual Permission Checks

def check_user_permissions(user, module, action):
    """Check if user has specific permission"""
    
    # Super admin has all permissions
    if user.is_super_admin:
        return True
        
    # Tenant admin has all permissions within tenant
    if user.is_tenant_admin:
        return True
        
    # Check employee permissions
    try:
        employee_profile = user.tenant_employee_profile
        return employee_profile.has_permission(module, action)
    except AttributeError:
        return False

# Usage
if check_user_permissions(request.user, 'employees', 'write'):
    # User can modify employee data
    pass

Using Permission Manager

from client.decorators import PermissionManager

# Get all user permissions
permissions = PermissionManager.get_user_permissions(user)

# Check specific permission
if PermissionManager.has_permission(user, 'hr', 'read'):
    # User can view HR data
    pass

# Check module access
if PermissionManager.has_module_access(user, 'finance'):
    # User can access finance module
    pass

3. ViewSet Implementation Examples

Protected ViewSet with Dynamic Permissions

class ProtectedViewSet(viewsets.ModelViewSet):
    """Base viewset with automatic permission checking"""
    
    permission_module = None  # Override in subclasses
    
    def get_permissions(self):
        """Automatic permission based on action"""
        if not self.permission_module:
            return [IsAuthenticated()]
            
        action_permission_map = {
            'list': 'read',
            'retrieve': 'read',
            'create': 'write',
            'update': 'write',
            'partial_update': 'write',
            'destroy': 'delete'
        }
        
        permission_type = action_permission_map.get(self.action, 'read')
        
        # Create dynamic permission class
        class DynamicPermission(RoleBasedPermission):
            module = self.permission_module
            permission_type = permission_type
        
        return [IsAuthenticated(), TenantPermission(), DynamicPermission()]

# Usage
class InventoryViewSet(ProtectedViewSet):
    permission_module = 'inventory'
    queryset = InventoryItem.objects.all()
    serializer_class = InventorySerializer

Branch-Specific ViewSet

class BranchRestrictedViewSet(viewsets.ModelViewSet):
    """ViewSet that restricts data to user's branch"""
    
    permission_classes = [IsAuthenticated, TenantPermission, EmployeePermission]
    
    def get_queryset(self):
        """Filter queryset based on user's branch access"""
        user = self.request.user
        
        if user.is_super_admin:
            return self.queryset
        elif user.is_tenant_admin:
            return self.queryset  # Access to all branches in tenant
        elif user.is_employee:
            # Restrict to user's branch
            try:
                employee_meta = user.employee_meta
                return self.queryset.filter(branch_id=employee_meta.branch_id)
            except AttributeError:
                return self.queryset.none()
        
        return self.queryset.none()

4. Custom Actions with Permissions

class EmployeeViewSet(viewsets.ModelViewSet):
    queryset = EmployeeProfile.objects.all()
    serializer_class = EmployeeSerializer
    
    @action(detail=True, methods=['post'])
    @require_permission('employees', 'write')
    def assign_role(self, request, pk=None):
        """Assign role to employee"""
        employee = self.get_object()
        role_id = request.data.get('role_id')
        
        try:
            role = Role.objects.get(id=role_id, is_active=True)
            employee.assign_role(role)
            
            return Response({
                'message': f'Role {role.name} assigned to {employee.full_name}',
                'success': True
            })
        except Role.DoesNotExist:
            return Response({
                'error': 'Role not found',
                'success': False
            }, status=404)
    
    @action(detail=False, methods=['get'])
    @require_module_access('employees')
    def statistics(self, request):
        """Get employee statistics"""
        # Only users with employee module access can view stats
        stats = {
            'total_employees': EmployeeProfile.objects.filter(is_active=True).count(),
            'by_department': self.get_department_stats(),
            'by_branch': self.get_branch_stats(),
        }
        return Response(stats)

API Security & Access Control

1. Authentication & Authorization Flow

sequenceDiagram
    participant Client
    participant Middleware
    participant Permission
    participant Database
    
    Client->>Middleware: Request with JWT Token
    Middleware->>Middleware: Validate Token
    Middleware->>Middleware: Set Tenant Context
    Middleware->>Permission: Check Permissions
    Permission->>Database: Query User Roles
    Permission->>Database: Query Role Permissions
    Permission->>Permission: Cache Results
    Permission->>Middleware: Return Permission Result
    Middleware->>Client: Response or 403 Forbidden
Loading

2. API Endpoint Security

Secure API Endpoints

# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter

# Protected endpoints
router = DefaultRouter()
router.register(r'employees', EmployeeViewSet, basename='employee')
router.register(r'branches', BranchViewSet, basename='branch')
router.register(r'roles', RoleViewSet, basename='role')

urlpatterns = [
    path('api/', include(router.urls)),
    path('api/auth/', include('rest_framework.urls')),
]

Permission-Based URL Routing

# Custom URL patterns with permissions
urlpatterns = [
    # Super admin only
    path('admin/tenants/', SuperAdminTenantViewSet.as_view({'get': 'list'}), 
         name='admin-tenants'),
    
    # Tenant admin only
    path('admin/employees/', TenantAdminEmployeeViewSet.as_view({'get': 'list'}), 
         name='admin-employees'),
    
    # Employee with permissions
    path('employees/<uuid:pk>/profile/', EmployeeProfileView.as_view(), 
         name='employee-profile'),
]

3. Middleware Integration

class TenantPermissionMiddleware:
    """Middleware for tenant-based permission checking"""
    
    def __init__(self, get_response):
        self.get_response = get_response
        
    def __call__(self, request):
        # Set tenant context
        if request.user.is_authenticated and not request.user.is_super_admin:
            current_tenant = getattr(request, 'tenant', None)
            if request.user.tenant != current_tenant:
                return HttpResponseForbidden('Access denied')
        
        response = self.get_response(request)
        return response

4. Security Headers & CORS

# settings.py
CORS_ALLOWED_ORIGINS = [
    "https://tenant1.yourdomain.com",
    "https://tenant2.yourdomain.com",
]

# Security headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# Custom middleware for additional security
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django_tenants.middleware.main.TenantMainMiddleware',
    'core.middleware.TenantPermissionMiddleware',
    # ... other middleware
]

Advanced Permission Patterns

1. Conditional Permissions

class ConditionalPermission(BasePermission):
    """Permission that depends on business conditions"""
    
    def has_permission(self, request, view):
        user = request.user
        
        # Time-based permissions
        if hasattr(view, 'time_restricted') and view.time_restricted:
            current_hour = timezone.now().hour
            if not (9 <= current_hour <= 17):  # Business hours only
                return False
        
        # Department-based permissions
        if hasattr(view, 'department_restricted'):
            try:
                employee = user.tenant_employee_profile
                if employee.department != view.department_restricted:
                    return False
            except AttributeError:
                return False
        
        return True

2. Permission Inheritance

class InheritedPermission(RoleBasedPermission):
    """Permission that inherits from parent roles"""
    
    def has_permission(self, request, view):
        # Check direct permission
        if super().has_permission(request, view):
            return True
            
        # Check inherited permissions from manager
        try:
            employee = request.user.tenant_employee_profile
            if employee.manager:
                # Check if manager has permission
                return employee.manager.has_permission(self.module, self.permission_type)
        except AttributeError:
            pass
            
        return False

3. Audit Trail Integration

class AuditedPermission(BasePermission):
    """Permission with audit logging"""
    
    def has_permission(self, request, view):
        result = super().has_permission(request, view)
        
        # Log permission check
        AuditLog.objects.create(
            user=request.user,
            action=f"permission_check_{self.__class__.__name__}",
            result=result,
            ip_address=request.META.get('REMOTE_ADDR'),
            user_agent=request.META.get('HTTP_USER_AGENT'),
            metadata={
                'module': getattr(self, 'module', None),
                'permission_type': getattr(self, 'permission_type', None),
                'view': view.__class__.__name__,
                'method': request.method,
            }
        )
        
        return result

4. Rate Limiting Integration

class RateLimitedPermission(BasePermission):
    """Permission with rate limiting"""
    
    def has_permission(self, request, view):
        # Check rate limit
        cache_key = f"rate_limit_{request.user.id}_{view.__class__.__name__}"
        current_requests = cache.get(cache_key, 0)
        
        if current_requests >= settings.API_RATE_LIMIT:
            return False
            
        # Increment counter
        cache.set(cache_key, current_requests + 1, 3600)  # 1 hour
        
        return super().has_permission(request, view)

Testing & Debugging

1. Permission Testing

# tests/test_permissions.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from client.models import Role, Permission, EmployeeProfile

User = get_user_model()

class PermissionTestCase(TestCase):
    def setUp(self):
        """Set up test data"""
        # Create permissions
        self.read_perm = Permission.objects.create(
            name="Read Employees",
            codename="employees.read",
            module="employees",
            permission_type="read"
        )
        
        # Create role
        self.employee_role = Role.objects.create(name="Employee")
        self.employee_role.permissions.add(self.read_perm)
        
        # Create users
        self.tenant_admin = User.objects.create_user(
            email="admin@test.com",
            password="password",
            user_type="tenant_admin"
        )
        
        self.employee_user = User.objects.create_user(
            email="employee@test.com",
            password="password",
            user_type="employee"
        )
    
    def test_employee_permissions(self):
        """Test employee permission checking"""
        # Create employee profile
        employee = EmployeeProfile.objects.create(
            user=self.employee_user,
            branch=self.branch,
            department="IT"
        )
        employee.roles.add(self.employee_role)
        
        # Test permissions
        self.assertTrue(employee.has_permission("employees", "read"))
        self.assertFalse(employee.has_permission("employees", "write"))
    
    def test_permission_caching(self):
        """Test permission caching"""
        from client.decorators import PermissionManager
        
        # Clear cache
        PermissionManager.clear_user_permissions_cache(self.employee_user)
        
        # Check permissions (should cache)
        result1 = PermissionManager.has_permission(self.employee_user, "employees", "read")
        
        # Check again (should use cache)
        result2 = PermissionManager.has_permission(self.employee_user, "employees", "read")
        
        self.assertEqual(result1, result2)

2. Debug Utilities

# utils/debug_permissions.py
def debug_user_permissions(user):
    """Debug utility to show all user permissions"""
    print(f"=== Permissions for {user.email} ===")
    print(f"User type: {user.user_type}")
    print(f"Is super admin: {user.is_super_admin}")
    print(f"Is tenant admin: {user.is_tenant_admin}")
    
    if hasattr(user, 'tenant_employee_profile'):
        employee = user.tenant_employee_profile
        print(f"Employee roles: {[role.name for role in employee.roles.all()]}")
        
        # Show all permissions by module
        permissions = PermissionManager.get_user_permissions(user, cached=False)
        for module, perms in permissions.items():
            print(f"  {module}: {perms}")
    
    print("=" * 50)

# Usage in views or management commands
debug_user_permissions(request.user)

3. Permission Debugging Decorator

def debug_permissions(func):
    """Decorator to debug permission checks"""
    def wrapper(*args, **kwargs):
        request = args[0] if args else None
        if request and hasattr(request, 'user'):
            print(f"Permission check for {request.user.email}")
            print(f"Path: {request.path}")
            print(f"Method: {request.method}")
            
        result = func(*args, **kwargs)
        print(f"Permission result: {result}")
        return result
    return wrapper

# Usage
@debug_permissions
def has_permission(self, request, view):
    return super().has_permission(request, view)

Best Practices & Performance

1. Permission Caching Strategy

class PermissionManager:
    """Centralized permission management with caching"""
    
    @staticmethod
    def get_user_permissions(user, cached=True):
        """Get all user permissions with caching"""
        if not cached:
            return PermissionManager._fetch_permissions(user)
            
        cache_key = f"user_permissions_{user.id}"
        permissions = cache.get(cache_key)
        
        if permissions is None:
            permissions = PermissionManager._fetch_permissions(user)
            cache.set(cache_key, permissions, 300)  # 5 minutes
            
        return permissions
    
    @staticmethod
    def clear_user_permissions_cache(user):
        """Clear cached permissions for user"""
        cache_key = f"user_permissions_{user.id}"
        cache.delete(cache_key)
        
        # Clear related caches
        cache.delete_many([
            f"role_perm_{user.id}_{module}_{perm_type}"
            for module in ['employees', 'hr', 'finance']
            for perm_type in ['read', 'write', 'delete']
        ])

2. Database Optimization

# Optimize permission queries
def get_user_roles_with_permissions(user):
    """Optimized query for user roles and permissions"""
    return Role.objects.filter(
        employeeprofile__user=user,
        is_active=True
    ).prefetch_related(
        'permissions'
    ).select_related()

# Use database indexes
class Permission(models.Model):
    # ... fields ...
    
    class Meta:
        indexes = [
            models.Index(fields=['module', 'permission_type']),
            models.Index(fields=['codename']),
            models.Index(fields=['is_active']),
        ]

class Role(models.Model):
    # ... fields ...
    
    class Meta:
        indexes = [
            models.Index(fields=['name', 'is_active']),
            models.Index(fields=['is_system_role']),
        ]

3. Security Best Practices

# 1. Always validate tenant context
def validate_tenant_access(user, requested_tenant):
    """Ensure user can access requested tenant"""
    if user.is_super_admin:
        return True
    return user.tenant == requested_tenant

# 2. Use proper permission inheritance
class SecureViewSet(viewsets.ModelViewSet):
    def get_permissions(self):
        """Always include base security permissions"""
        base_permissions = [IsAuthenticated, TenantPermission]
        
        # Add specific permissions based on action
        if self.action in ['create', 'update', 'partial_update']:
            base_permissions.append(EmployeeWritePermission)
        
        return [permission() for permission in base_permissions]

# 3. Implement audit logging
def log_permission_check(user, permission, result):
    """Log all permission checks for audit"""
    logger.info(
        f"Permission check: {user.email} - {permission} - {result}",
        extra={
            'user_id': user.id,
            'permission': permission,
            'result': result,
            'timestamp': timezone.now()
        }
    )

4. Performance Monitoring

# Monitor permission check performance
class PerformanceMonitoringPermission(BasePermission):
    """Permission with performance monitoring"""
    
    def has_permission(self, request, view):
        start_time = time.time()
        
        result = super().has_permission(request, view)
        
        duration = time.time() - start_time
        
        # Log slow permission checks
        if duration > 0.1:  # 100ms threshold
            logger.warning(
                f"Slow permission check: {duration:.3f}s",
                extra={
                    'user_id': request.user.id,
                    'permission_class': self.__class__.__name__,
                    'duration': duration,
                    'view': view.__class__.__name__
                }
            )
        
        return result

Conclusion

This comprehensive permission system provides:

  1. Multi-tenant Security: Complete data isolation between tenants
  2. Role-Based Access Control: Flexible permission management through roles
  3. Granular Permissions: Fine-grained control over actions and modules
  4. Performance Optimization: Intelligent caching and database optimization
  5. Audit Trail: Complete logging of permission checks and changes
  6. Extensibility: Easy to add new modules and permission types

The system is designed to scale with your application while maintaining security and performance. Follow the patterns and best practices outlined in this guide to implement robust permission control in your multi-tenant ERP application.

Key Takeaways:

  • Always validate tenant context before checking permissions
  • Use caching strategically to improve performance
  • Implement proper audit logging for security compliance
  • Follow the principle of least privilege
  • Test permissions thoroughly in all scenarios
  • Monitor performance and optimize as needed

This permission system forms the foundation for secure, scalable multi-tenant applications with comprehensive access control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment