mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
1. 将所有初始化数据文件从app/scripts/data迁移至backend/sql/data目录 2. 更新path_conf.py中的SCRIPT_DIR配置指向新路径 3. 移除app/scripts/data下的冗余初始化数据文件 4. 清理权限缓存相关的进程级缓存逻辑 5. 调整部分代码导入顺序和冗余依赖 6. 简化用户菜单获取逻辑,移除菜单树缓存 7. 调整套餐菜单设置接口,移除redis参数传递
215 lines
8.8 KiB
Python
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
|