Files
FastapiAdmin/backend/app/core/permission.py
T
zhangtao 8d0e76c694 refactor: 统一初始化数据文件存放路径并清理冗余脚本数据
1. 将所有初始化数据文件从app/scripts/data迁移至backend/sql/data目录
2. 更新path_conf.py中的SCRIPT_DIR配置指向新路径
3. 移除app/scripts/data下的冗余初始化数据文件
4. 清理权限缓存相关的进程级缓存逻辑
5. 调整部分代码导入顺序和冗余依赖
6. 简化用户菜单获取逻辑,移除菜单树缓存
7. 调整套餐菜单设置接口,移除redis参数传递
2026-07-13 01:38:51 +08:00

215 lines
8.8 KiB
Python

from __future__ import annotations
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
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, db: AsyncSession) -> None:
"""初始化权限过滤器实例"""
self.model = model
self.auth = auth
self.db = db
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:
"""根据模型的权限过滤策略,选择合适的过滤方法
注意:当 ``auth.user.is_superuser=True`` 时直接返回 ``None``(不应用任何过滤),
以便平台超管能查看/管理所有角色的菜单授权(``USER_BINDING`` 等策略对超管不生效)。
"""
if not self.auth.user or not self.auth.user.id or not self.auth.check_data_scope or self.auth.user.is_superuser:
return None
strategy = getattr(self.model, "__permission_strategy__", PermissionFilterStrategy.DATA_SCOPE)
method = {
PermissionFilterStrategy.MENU_AUTH: self.__filter_by_menu_auth,
PermissionFilterStrategy.DEPT_RELATION: self.__filter_by_dept_relation,
PermissionFilterStrategy.OWN: self.__filter_by_own,
PermissionFilterStrategy.USER_BINDING: self.__filter_by_user_binding,
}.get(strategy, self.__filter_by_data_scope)
return await method()
async def __filter_by_menu_auth(self) -> ColumnElement | None:
"""基于角色-菜单授权的过滤(适用于菜单模型)
只显示用户角色授权的菜单,同时受租户套餐约束。
"""
menu_ids = set(self.auth.menu_ids)
if not menu_ids:
return self.__id_eq(-1)
if self.auth.user and self.auth.user.tenant_id:
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
cached = set[int](await PackageService(self.auth, self.db).get_tenant_available_menu_ids(self.auth.user.tenant_id))
object.__setattr__(self.auth, cache_attr, cached)
menu_ids = menu_ids & cached
return self.__id_in(menu_ids) if menu_ids else self.__id_eq(-1)
async def __filter_by_user_binding(self) -> ColumnElement | None:
"""基于当前用户绑定角色的过滤(适用于角色模型)
只显示当前用户绑定的角色。超管场景下不应用此过滤(参见 `__permission_condition`)。
"""
role_ids = self.auth.role_ids
return self.__id_in(role_ids) if role_ids else self.__id_eq(-1)
async def __filter_by_dept_relation(self) -> ColumnElement | None:
"""基于部门关联的过滤(适用于部门模型、用户模型)
根据用户的部门权限范围过滤数据
"""
assert self.auth.user is not None
data_scopes = set(self.auth.data_scopes)
custom_dept_ids = set(self.auth.custom_dept_ids)
if not data_scopes:
# 无数据权限范围:仅能看本部门
user_dept_id = self.auth.user.dept_id
return self.__id_eq(user_dept_id) if user_dept_id else None
if self.DATA_SCOPE_ALL in data_scopes:
return None
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)
if self.model.__name__ == "UserModel":
return self.__filter_user_model(accessible_dept_ids)
return None
async def __filter_by_own(self) -> ColumnElement | None:
"""仅本人数据过滤"""
assert self.auth.user is not None
return self.__created_id_eq(self.auth.user.id)
async def __filter_by_data_scope(self) -> ColumnElement | None:
"""基于数据范围权限的通用过滤(默认策略)
适用于大多数业务模型
"""
assert self.auth.user is not None
created_id_attr = getattr(self.model, "created_id", None)
if created_id_attr is None:
return None
data_scopes = set(self.auth.data_scopes)
custom_dept_ids = set(self.auth.custom_dept_ids)
if not data_scopes or self.DATA_SCOPE_SELF in data_scopes:
return created_id_attr == self.auth.user.id
if self.DATA_SCOPE_ALL in data_scopes:
return None
accessible_dept_ids = await self.__get_accessible_dept_ids(data_scopes, custom_dept_ids)
if not accessible_dept_ids:
return created_id_attr == self.auth.user.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[int](accessible_dept_ids))
creator_rel = getattr(self.model, "created_by", None)
if creator_rel is not None:
from app.api.v1.module_system.user.model import UserModel
return creator_rel.has(UserModel.dept_id.in_(list(accessible_dept_ids)))
return created_id_attr == self.auth.user.id
async def __get_accessible_dept_ids(self, data_scopes: set[int], custom_dept_ids: set[int]) -> set[int]:
"""获取用户可访问的所有部门ID"""
assert self.auth.user is not None
accessible_dept_ids: set[int] = set(custom_dept_ids)
user_dept_id = self.auth.user.dept_id
if self.DATA_SCOPE_DEPT in data_scopes and user_dept_id is not None:
accessible_dept_ids.add(user_dept_id)
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_objs = (await self.db.execute(select(DeptModel))).scalars().all()
id_map = get_child_id_map(dept_objs)
accessible_dept_ids.update(get_child_recursion(id=user_dept_id, id_map=id_map))
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:
"""过滤部门模型"""
assert self.auth.user is not None
if accessible_dept_ids:
return self.__id_in(accessible_dept_ids)
user_dept_id = self.auth.user.dept_id
return self.__id_eq(user_dept_id) if user_dept_id else None
def __filter_user_model(self, accessible_dept_ids: set[int]) -> ColumnElement | None:
"""过滤用户模型"""
if not accessible_dept_ids:
return None
dept_id_attr = getattr(self.model, "dept_id", None)
return dept_id_attr.in_(list(accessible_dept_ids)) if dept_id_attr is not None else None
def __id_eq(self, value: int | None) -> ColumnElement | None:
"""主键等于指定值"""
if value is None:
return None
id_attr = getattr(self.model, "id", None)
return id_attr == value if id_attr is not None else None
def __id_in(self, values: set[int] | list[int]) -> ColumnElement | None:
"""主键在指定集合中"""
if not values:
return None
id_attr = getattr(self.model, "id", None)
return id_attr.in_(list(values)) if id_attr is not None else None
def __created_id_eq(self, value: int) -> ColumnElement | None:
"""created_id 等于指定值"""
created_id_attr = getattr(self.model, "created_id", None)
return created_id_attr == value if created_id_attr is not None else None