You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

324 lines
12 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from typing import Any
from sqlalchemy import select
from sqlalchemy.sql.elements import ColumnElement
from app.common.enums import PermissionFilterStrategy
from app.core.base_schema import AuthSchema
from app.utils.common_util import get_child_id_map, get_child_recursion
class Permission:
"""
为业务模型提供数据权限过滤功能
使用策略模式,根据模型的 __permission_strategy__ 属性选择合适的过滤策略
"""
# 数据权限常量定义,提高代码可读性
DATA_SCOPE_SELF = 1 # 仅本人数据
DATA_SCOPE_DEPT = 2 # 本部门数据
DATA_SCOPE_DEPT_AND_CHILD = 3 # 本部门及以下数据
DATA_SCOPE_ALL = 4 # 全部数据
DATA_SCOPE_CUSTOM = 5 # 自定义数据
def __init__(self, model: Any, auth: AuthSchema) -> None:
"""
初始化权限过滤器实例
Args:
db: 数据库会话
model: 数据模型类
current_user: 当前用户对象
auth: 认证信息对象
"""
self.model = model
self.auth = auth
self.conditions: list[ColumnElement] = [] # 权限条件列表
async def filter_query(self, query: Any) -> Any:
"""
按数据权限为 SQLAlchemy 查询追加 WHERE 条件。
参数:
- query (Any): SQLAlchemy 查询对象。
返回:
- Any: 附加条件后的查询对象(无权限条件时原样返回)。
"""
condition = await self.__permission_condition()
return query.where(condition) if condition is not None else query
async def __permission_condition(self) -> ColumnElement | None:
"""
应用数据范围权限隔离
根据模型的权限过滤策略,选择合适的过滤方法
"""
# 如果不需要检查数据权限,则不限制
if not self.auth.user:
return None
# 如果检查数据权限为False,则不限制
if not self.auth.check_data_scope:
return None
# 超级管理员可以查看所有数据
if self.auth.user.is_superuser:
return None
# 获取模型的权限过滤策略
strategy = getattr(
self.model, "__permission_strategy__", PermissionFilterStrategy.DATA_SCOPE
)
# 根据策略选择过滤方法
if strategy == PermissionFilterStrategy.MENU_AUTH:
return await self.__filter_by_menu_auth()
elif strategy == PermissionFilterStrategy.DEPT_RELATION:
return await self.__filter_by_dept_relation()
elif strategy == PermissionFilterStrategy.OWN:
return await self.__filter_by_own()
elif strategy == PermissionFilterStrategy.USER_BINDING:
return await self.__filter_by_user_binding()
else:
return await self.__filter_by_data_scope()
async def __filter_by_menu_auth(self) -> ColumnElement | None:
"""
基于角色-菜单授权的过滤(适用于菜单模型)
只显示用户角色授权的菜单,同时受租户套餐约束。
"""
roles = getattr(self.auth.user, "roles", []) or []
if not roles:
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr == -1
return None
menu_ids = set()
for role in roles:
if hasattr(role, "menus") and role.menus:
menu_ids.update(menu.id for menu in role.menus if menu.status == 0)
# 租户用户:菜单列表也受套餐约束(请求级缓存避免重复 DB 查询)
if self.auth.tenant_id and menu_ids:
cache_attr = "_cached_package_menu_ids"
cached = getattr(self.auth, cache_attr, None)
if cached is None:
from app.api.v1.module_platform.package.service import PackageService
allowed_ids = set(await PackageService.get_tenant_available_menu_ids(self.auth, self.auth.tenant_id))
object.__setattr__(self.auth, cache_attr, allowed_ids)
else:
allowed_ids = cached
menu_ids = menu_ids & allowed_ids
if menu_ids:
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr.in_(list(menu_ids))
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr == -1
return None
async def __filter_by_user_binding(self) -> ColumnElement | None:
"""
基于当前用户绑定角色的过滤(适用于角色模型)
只显示当前用户绑定的角色
"""
roles = getattr(self.auth.user, "roles", []) or []
if not roles:
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr == -1
return None
role_ids = [role.id for role in roles]
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr.in_(role_ids)
return None
async def __filter_by_dept_relation(self) -> ColumnElement | None:
"""
基于部门关联的过滤(适用于部门模型、角色模型)
根据用户的部门权限范围过滤数据
"""
# 如果用户没有角色,则只能查看自己部门的数据
roles = getattr(self.auth.user, "roles", []) or []
if not roles:
user_dept_id = getattr(self.auth.user, "dept_id", None)
if user_dept_id is not None and hasattr(self.model, "id"):
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr == user_dept_id
return None
# 获取用户所有角色的权限范围
data_scopes = set()
custom_dept_ids = set()
for role in roles:
data_scopes.add(role.data_scope)
if role.data_scope == self.DATA_SCOPE_CUSTOM and hasattr(role, "depts") and role.depts:
custom_dept_ids.update(dept.id for dept in role.depts)
# 全部数据权限最高优先级
if self.DATA_SCOPE_ALL in data_scopes:
return None
# 收集所有可访问的部门ID
accessible_dept_ids = await self.__get_accessible_dept_ids(data_scopes, custom_dept_ids)
# 根据模型类型过滤
if self.model.__name__ == "DeptModel":
return self.__filter_dept_model(accessible_dept_ids)
elif self.model.__name__ == "UserModel":
return self.__filter_user_model(accessible_dept_ids)
else:
return None
async def __filter_by_own(self) -> ColumnElement | None:
"""
仅本人数据过滤
"""
created_id_attr = getattr(self.model, "created_id", None)
if created_id_attr is not None and self.auth.user:
return created_id_attr == self.auth.user.id
return None
async def __filter_by_data_scope(self) -> ColumnElement | None:
"""
基于数据范围权限的通用过滤(默认策略)
适用于大多数业务模型
"""
from app.api.v1.module_system.user.model import UserModel
# 如果模型没有创建人created_id字段,则不限制
if not hasattr(self.model, "created_id"):
return None
# 如果用户没有角色,则只能查看自己的数据
roles = getattr(self.auth.user, "roles", []) or []
if not roles:
created_id_attr = getattr(self.model, "created_id", None)
if created_id_attr is not None and self.auth.user:
return created_id_attr == self.auth.user.id
return None
# 获取用户所有角色的权限范围
data_scopes = set()
custom_dept_ids = set()
for role in roles:
data_scopes.add(role.data_scope)
if role.data_scope == self.DATA_SCOPE_CUSTOM and hasattr(role, "depts") and role.depts:
custom_dept_ids.update(dept.id for dept in role.depts)
# 全部数据权限最高优先级
if self.DATA_SCOPE_ALL in data_scopes:
return None
# 收集所有可访问的部门ID
accessible_dept_ids = await self.__get_accessible_dept_ids(data_scopes, custom_dept_ids)
# 如果有部门权限,使用部门过滤
if accessible_dept_ids:
# 特殊处理如果模型本身就是UserModel直接过滤用户的dept_id
if self.model.__name__ == "UserModel" and hasattr(self.model, "dept_id"):
dept_id_attr = getattr(self.model, "dept_id", None)
if dept_id_attr is not None:
return dept_id_attr.in_(list(accessible_dept_ids))
# 其他模型通过created_by关系过滤创建人的部门
creator_rel = getattr(self.model, "created_by", None)
if creator_rel is not None and hasattr(UserModel, "dept_id"):
return creator_rel.has(UserModel.dept_id.in_(list(accessible_dept_ids)))
# 降级方案:只能查看自己的数据
created_id_attr = getattr(self.model, "created_id", None)
if created_id_attr is not None and self.auth.user:
return created_id_attr == self.auth.user.id
return None
# 处理仅本人数据权限
if self.DATA_SCOPE_SELF in data_scopes:
created_id_attr = getattr(self.model, "created_id", None)
if created_id_attr is not None and self.auth.user:
return created_id_attr == self.auth.user.id
return None
# 默认情况:只能查看自己的数据
created_id_attr = getattr(self.model, "created_id", None)
if created_id_attr is not None and self.auth.user:
return created_id_attr == self.auth.user.id
return None
async def __get_accessible_dept_ids(self, data_scopes: set, custom_dept_ids: set) -> set[int]:
"""
获取用户可访问的所有部门ID
Args:
data_scopes: 用户角色的数据权限范围集合
custom_dept_ids: 自定义权限关联的部门ID集合
Returns:
可访问的部门ID集合
"""
accessible_dept_ids = set()
user_dept_id = getattr(self.auth.user, "dept_id", None)
# 处理自定义数据权限5
if self.DATA_SCOPE_CUSTOM in data_scopes:
accessible_dept_ids.update(custom_dept_ids)
# 处理本部门数据权限2
if self.DATA_SCOPE_DEPT in data_scopes and user_dept_id is not None:
accessible_dept_ids.add(user_dept_id)
# 处理本部门及以下数据权限3
if self.DATA_SCOPE_DEPT_AND_CHILD in data_scopes and user_dept_id is not None:
try:
from app.api.v1.module_system.dept.model import DeptModel
dept_sql = select(DeptModel)
dept_result = await self.auth.db.execute(dept_sql)
dept_objs = dept_result.scalars().all()
id_map = get_child_id_map(dept_objs)
dept_with_children_ids = get_child_recursion(id=user_dept_id, id_map=id_map)
accessible_dept_ids.update(dept_with_children_ids)
except Exception:
accessible_dept_ids.add(user_dept_id)
return accessible_dept_ids
def __filter_dept_model(self, accessible_dept_ids: set[int]) -> ColumnElement | None:
"""
过滤部门模型
"""
if accessible_dept_ids:
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr.in_(list(accessible_dept_ids))
user_dept_id = getattr(self.auth.user, "dept_id", None)
if user_dept_id is not None:
id_attr = getattr(self.model, "id", None)
if id_attr is not None:
return id_attr == user_dept_id
return None
def __filter_user_model(self, accessible_dept_ids: set[int]) -> ColumnElement | None:
"""
过滤用户模型
"""
if accessible_dept_ids:
dept_id_attr = getattr(self.model, "dept_id", None)
if dept_id_attr is not None:
return dept_id_attr.in_(list(accessible_dept_ids))
return None