- Overview & Architecture
- Theoretical Concepts
- User Types & Hierarchies
- Permission System Components
- Role-Based Access Control (RBAC)
- Permission Classes & Implementation
- Using Custom Permissions in ViewSets
- Code Examples & Implementation
- API Security & Access Control
- Advanced Permission Patterns
- Testing & Debugging
- Best Practices & Performance
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.
- 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
┌─────────────────────────────────────────────────────────────────┐
│ 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) │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
│ └─────────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────┘
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
RBAC follows the principle of least privilege:
- Users are assigned Roles
- Roles contain Permissions
- Permissions define Actions on Modules
Module → Permission Type → Action
│ │ │
│ │ └─ read, write, delete, execute
│ │
│ └─ Granular control per operation
│
└─ System modules (hr, finance, employees, etc.)
- Authentication: User must be logged in
- Tenant Isolation: User can only access their tenant's data
- Role Verification: User must have required role
- Permission Check: Role must have specific permission
- Object-Level Access: Additional checks for specific objects
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 checksDatabase Storage: Public schema only
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 tenantDatabase Storage:
- User record: Public schema
- Profile:
ClientAdminProfile(Public schema)
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)
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)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()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)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 FalseEach 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
]# 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)# 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)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
})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_adminclass 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_tenantclass 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_permclass 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'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.moduleclass 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 Falsefrom 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]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
passclass 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]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]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# 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()@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 employeedef 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
passfrom 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
passclass 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 = InventorySerializerclass 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()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)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
# 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')),
]# 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'),
]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# 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
]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 Trueclass 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 Falseclass 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 resultclass 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)# 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)# 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)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)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']
])# 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']),
]# 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()
}
)# 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 resultThis comprehensive permission system provides:
- Multi-tenant Security: Complete data isolation between tenants
- Role-Based Access Control: Flexible permission management through roles
- Granular Permissions: Fine-grained control over actions and modules
- Performance Optimization: Intelligent caching and database optimization
- Audit Trail: Complete logging of permission checks and changes
- 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.
- 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.