Coverage for core\permissions.py: 0%
46 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
1def require_tenant(request):
2 """Ensure that the request has an active tenant/organization context."""
3 tenant_id = getattr(request, 'tenant_id', None)
4 if not tenant_id and hasattr(request, 'user') and request.user.is_authenticated:
5 tenant_id = getattr(request.user, 'current_organization_id', None)
6 request.tenant_id = tenant_id
8 if not tenant_id:
9 from core.exceptions import BadRequestException
10 raise BadRequestException("No active organization context found. Set X-Tenant-ID header.")
11 return tenant_id
13def is_org_admin(user, organization_id) -> bool:
14 """Check if user has admin/owner privileges in the organization."""
15 if user.is_superuser:
16 return True
17 from modules.team.models import TeamMember
18 member = TeamMember.objects.filter(
19 user=user,
20 tenant_id=organization_id,
21 role__in=['OWNER', 'ADMIN']
22 ).first()
23 return member is not None
26def get_user_role(user, organization_id) -> str | None:
27 """Get user's role in the organization, or None if not a member."""
28 from modules.team.models import TeamMember
29 member = TeamMember.objects.filter(
30 user=user,
31 tenant_id=organization_id
32 ).first()
33 return member.role if member else None
36import functools
38def require_role(min_role: str):
39 """
40 Decorator factory that enforces a minimum role for an endpoint.
41 Roles (lowest to highest): GUEST < MEMBER < HR < MANAGER < ADMIN < OWNER
42 Superusers bypass this check.
43 """
44 ROLE_HIERARCHY = ['GUEST', 'MEMBER', 'HR', 'MANAGER', 'ADMIN', 'OWNER']
46 def decorator(func):
47 @functools.wraps(func)
48 def wrapper(request, *args, **kwargs):
49 tenant_id = require_tenant(request)
50 role = get_user_role(request.user, tenant_id)
52 # Superusers bypass role checks
53 if request.user.is_superuser:
54 return func(request, *args, **kwargs)
56 if role is None:
57 from core.exceptions import ForbiddenException
58 raise ForbiddenException("You are not a member of this organization.")
60 min_idx = ROLE_HIERARCHY.index(min_role)
61 user_idx = ROLE_HIERARCHY.index(role) if role in ROLE_HIERARCHY else -1
63 if user_idx < min_idx:
64 from core.exceptions import ForbiddenException
65 raise ForbiddenException(f"This action requires {min_role} role or higher.")
67 return func(request, *args, **kwargs)
68 return wrapper
69 return decorator
72def require_admin(func):
73 """Shorthand for require_role('ADMIN')"""
74 return require_role('ADMIN')(func)
77def require_manager(func):
78 """Shorthand for require_role('MANAGER')"""
79 return require_role('MANAGER')(func)
82def require_org_owner(func):
83 """Shorthand for require_role('OWNER')"""
84 return require_role('OWNER')(func)