Coverage for core/rbac.py: 95.06%
81 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-03 15:30 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-03 15:30 +0000
1import logging
2from functools import wraps
4from fastapi import HTTPException, status
5from sqlalchemy import func, select
6from sqlalchemy.ext.asyncio import AsyncSession
8from core.config import settings
9from models.role_attributes import RoleAttributes
10from models.role_attributes_mapper import RoleAttributesMapper
11from models.role_mapper import RoleMapper
12from models.roles import Roles
13from utils.custom_exception import ServerException
15logger = logging.getLogger("rbac")
18def is_super_admin_role_name(role_name: str | None) -> bool:
19 """True when role_name is the ENV-configured system super-admin role."""
20 return bool(role_name) and role_name == settings.DEFAULT_SUPER_ADMIN_ROLE
23async def get_user_role_level(user_id: str, db: AsyncSession) -> int:
24 """Return the user's highest role level, or 0 when the user has no role."""
25 try:
26 result = await db.execute(
27 select(func.max(Roles.level))
28 .join(RoleMapper, Roles.id == RoleMapper.role_id)
29 .where(RoleMapper.user_id == user_id)
30 )
31 level = result.scalar_one_or_none()
32 return int(level) if level is not None else 0
33 except Exception as e:
34 raise ServerException(f"Failed to get user role level: {e}")
37async def get_user_role_id(user_id: str, db: AsyncSession) -> str | None:
38 """Return the user's primary role id (highest level), or None when unassigned."""
39 try:
40 result = await db.execute(
41 select(Roles.id)
42 .join(RoleMapper, Roles.id == RoleMapper.role_id)
43 .where(RoleMapper.user_id == user_id)
44 .order_by(Roles.level.desc(), Roles.name.asc())
45 .limit(1)
46 )
47 return result.scalar_one_or_none()
48 except Exception as e:
49 raise ServerException(f"Failed to get user role id: {e}")
52async def user_has_role(user_id: str, role_id: str, db: AsyncSession) -> bool:
53 """True when the user is currently assigned the given role."""
54 try:
55 result = await db.execute(
56 select(RoleMapper.role_id).where(
57 RoleMapper.user_id == user_id,
58 RoleMapper.role_id == role_id,
59 )
60 )
61 return result.scalar_one_or_none() is not None
62 except Exception as e:
63 raise ServerException(f"Failed to check user role assignment: {e}")
66async def get_user_attributes(user_id: str, db: AsyncSession) -> dict[str, bool]:
67 """Get user attributes"""
68 try:
69 # Check if user has super admin role first
70 if await check_user_has_super_role(user_id, db):
71 # Super admin has all attributes - get all available attributes
72 all_attributes_result = await db.execute(select(RoleAttributes.name))
73 all_attributes = [row.name for row in all_attributes_result]
74 return {attr: True for attr in all_attributes}
76 result = await db.execute(
77 select(RoleAttributes.name, RoleAttributesMapper.value)
78 .join(RoleAttributesMapper, RoleAttributes.id == RoleAttributesMapper.attributes_id)
79 .join(RoleMapper, RoleMapper.role_id == RoleAttributesMapper.role_id)
80 .where(RoleMapper.user_id == user_id)
81 )
83 attributes = {}
84 for row in result:
85 attr_name = row.name
86 attr_value = row.value
88 if attr_name in attributes:
89 attributes[attr_name] = attributes[attr_name] or attr_value
90 else:
91 attributes[attr_name] = attr_value
93 return attributes
94 except Exception as e:
95 raise ServerException(f"Failed to get user attributes: {e}")
98async def check_user_has_super_role(user_id: str, db: AsyncSession) -> bool:
99 """Check if user has super admin role"""
100 try:
101 result = await db.execute(
102 select(Roles.name)
103 .join(RoleMapper, Roles.id == RoleMapper.role_id)
104 .where(RoleMapper.user_id == user_id)
105 )
107 user_roles = [row.name for row in result]
108 return settings.DEFAULT_SUPER_ADMIN_ROLE in user_roles
109 except Exception as e:
110 raise ServerException(f"Failed to check super role: {e}")
113def require_permission(required_attributes: list[str]):
114 """Permission check decorator"""
116 def decorator(func):
117 @wraps(func)
118 async def wrapper(*args, **kwargs):
119 token = kwargs.get("token")
120 db = kwargs.get("db")
122 if not db:
123 raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
125 user_id = token.get("sub")
127 # Check if user has super admin role first
128 if await check_user_has_super_role(user_id, db):
129 return await func(*args, **kwargs)
131 # If not super admin, check specific permissions
132 user_attributes = await get_user_attributes(user_id, db)
134 # Convert Permission enum to string value if needed
135 def get_attr_value(attr):
136 """Convert Permission enum to string value, or return as-is if already a string"""
137 if hasattr(attr, "value"):
138 return attr.value
139 return str(attr) if attr else attr
141 # Check if the user has at least one of the required permissions
142 attr_values = [get_attr_value(attr) for attr in required_attributes]
143 has_permission = any(
144 user_attributes.get(attr_value, False) for attr_value in attr_values
145 )
147 if not has_permission:
148 logger.warning(
149 f"Permission denied for user {user_id}. "
150 f"Required: {attr_values}, "
151 f"User has: {[k for k, v in user_attributes.items() if v]}"
152 )
153 raise HTTPException(
154 status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied"
155 )
157 return await func(*args, **kwargs)
159 return wrapper
161 return decorator