| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- """数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)
- data_scope 值:
- 1 = 全部数据
- 2 = 本部门数据
- 3 = 本部门及以下数据
- 4 = 仅本人数据
- 5 = 自定义(sys_role_data_scope_detail)
- """
- from __future__ import annotations
- from typing import List, Optional, Set, Tuple
- from fastapi import HTTPException
- from sqlalchemy import or_
- from sqlalchemy.orm import Session
- from app.core.config import settings
- from app.models.models import (
- SysDept,
- SysRole,
- SysRoleDataScopeDetail,
- SysUser,
- SysUserRole,
- )
- def get_user_dept_id(db: Session, user_id: str) -> str | None:
- """从 SysUser 读取用户的 dept_id。"""
- row = (
- db.query(SysUser.dept_id)
- .filter(SysUser.id == user_id, SysUser.del_flag == 0)
- .first()
- )
- return row[0] if row and row[0] else None
- def _get_child_dept_ids(db: Session, dept_id: str, tenant_no: str) -> List[str]:
- """递归获取 dept_id 及其所有子部门 ID(含自身)。"""
- result: List[str] = [dept_id]
- queue: List[str] = [dept_id]
- while queue:
- parents = queue
- queue = []
- children = (
- db.query(SysDept.id)
- .filter(
- SysDept.parent_id.in_(parents),
- SysDept.del_flag == 0,
- SysDept.tenant_no == tenant_no,
- )
- .all()
- )
- for child in children:
- cid = child[0]
- if cid not in result:
- result.append(cid)
- queue.append(cid)
- return result
- def _resolve_user_role_scopes(
- db: Session, user_id: str, tenant_no: str
- ) -> List[dict]:
- """获取用户所有角色的数据权限配置。
- 返回: [{"data_scope": str, "details": [SysRoleDataScopeDetail, ...]}]
- """
- roles = (
- db.query(SysRole)
- .join(SysUserRole, SysUserRole.role_id == SysRole.id)
- .filter(
- SysUserRole.user_id == user_id,
- SysUserRole.del_flag == 0,
- SysUserRole.tenant_no == tenant_no,
- SysRole.del_flag == 0,
- SysRole.status == "1",
- SysRole.tenant_no == tenant_no,
- )
- .all()
- )
- if not roles:
- return []
- result: List[dict] = []
- role_ids = [r.id for r in roles]
- all_details = (
- db.query(SysRoleDataScopeDetail)
- .filter(
- SysRoleDataScopeDetail.role_id.in_(role_ids),
- SysRoleDataScopeDetail.del_flag == 0,
- SysRoleDataScopeDetail.tenant_no == tenant_no,
- )
- .all()
- )
- details_by_role: dict[str, list] = {}
- for d in all_details:
- details_by_role.setdefault(d.role_id, []).append(d)
- for role in roles:
- result.append(
- {
- "data_scope": role.data_scope or "1",
- "details": details_by_role.get(role.id, []),
- }
- )
- return result
- def _collect_scope_ids(
- db: Session,
- user_info: dict,
- user_id: str,
- effective_tenant: str,
- role_scopes: List[dict],
- ) -> Tuple[Set[str], Set[str]]:
- """合并多角色数据范围为 dept_ids / user_ids 并集。"""
- dept_ids: Set[str] = set()
- user_ids: Set[str] = set()
- user_dept_id = user_info.get("dept_id") or user_info.get("region_id") or get_user_dept_id(
- db, user_id
- )
- for rs in role_scopes:
- scope = rs["data_scope"]
- if scope == "2":
- if user_dept_id:
- dept_ids.add(user_dept_id)
- elif scope == "3":
- if user_dept_id:
- dept_ids.update(_get_child_dept_ids(db, user_dept_id, effective_tenant))
- elif scope == "4":
- user_ids.add(user_id)
- elif scope == "5":
- for detail in rs["details"]:
- val = (detail.scope_value or "").strip()
- if not val:
- continue
- if detail.scope_type == "dept":
- dept_ids.add(val)
- elif detail.scope_type == "user":
- user_ids.add(val)
- return dept_ids, user_ids
- def _append_user_scope_conditions(
- conditions: list,
- model,
- user_ids: Set[str],
- *,
- has_user: bool,
- owner_field: Optional[str],
- ) -> None:
- if not user_ids:
- return
- user_conds = []
- if has_user:
- user_conds.append(model.create_by.in_(user_ids))
- if owner_field and hasattr(model, owner_field):
- user_conds.append(getattr(model, owner_field).in_(user_ids))
- if user_conds:
- conditions.append(or_(*user_conds))
- def apply_data_scope(
- query,
- model,
- user_info: dict,
- db: Session,
- tenant_no: str = None,
- owner_field: Optional[str] = None,
- ):
- """根据用户角色的数据权限过滤查询。
- 适用于业务单据列表/详情(报销单、客户、运单等)。
- 以下场景 SHALL NOT 调用本函数(仅租户隔离即可):
- - 下拉/选项接口(/options)
- - 租户级主数据树与码表(部门、字典、费用项目、产品分类、民族等)
- - 已有专用归属过滤的接口(如发票夹按 owner_user_id、待办按审批人)
- Args:
- query: SQLAlchemy query 对象
- model: 查询的主模型类(需要有 create_dept_id / create_by 字段)
- user_info: 当前用户信息字典,需含 role / user_id / dept_id 等
- db: 数据库 Session
- tenant_no: 租户编号,默认使用 settings.DEFAULT_TENANT_NO
- owner_field: 可选负责人字段名(如 CRM 的 owner_user_id),与 create_by 取 OR
- Returns:
- 应用数据权限过滤后的 query
- """
- if not user_info:
- return query
- if user_info.get("role") == "super_admin":
- return query
- user_id = user_info.get("user_id")
- if not user_id:
- return query
- effective_tenant = tenant_no or settings.DEFAULT_TENANT_NO
- has_dept = hasattr(model, "create_dept_id")
- has_user = hasattr(model, "create_by")
- if not has_dept and not has_user:
- return query
- role_scopes = _resolve_user_role_scopes(db, user_id, effective_tenant)
- if not role_scopes:
- return query.filter(False)
- if any(rs["data_scope"] == "1" for rs in role_scopes):
- return query
- dept_ids, user_ids = _collect_scope_ids(db, user_info, user_id, effective_tenant, role_scopes)
- conditions = []
- if dept_ids and has_dept:
- conditions.append(model.create_dept_id.in_(dept_ids))
- _append_user_scope_conditions(
- conditions, model, user_ids, has_user=has_user, owner_field=owner_field
- )
- if not conditions:
- return query.filter(False)
- return query.filter(or_(*conditions))
- def row_in_data_scope(
- db: Session,
- row,
- user_info: dict,
- tenant_no: Optional[str] = None,
- owner_field: Optional[str] = None,
- ) -> bool:
- """判断单行数据是否在当前用户数据权限范围内。"""
- if not user_info or row is None:
- return False
- if user_info.get("role") == "super_admin":
- return True
- user_id = user_info.get("user_id")
- if not user_id:
- return False
- effective_tenant = tenant_no or settings.DEFAULT_TENANT_NO
- role_scopes = _resolve_user_role_scopes(db, user_id, effective_tenant)
- if not role_scopes:
- return False
- if any(rs["data_scope"] == "1" for rs in role_scopes):
- return True
- dept_ids, user_ids = _collect_scope_ids(db, user_info, user_id, effective_tenant, role_scopes)
- row_dept = (getattr(row, "create_dept_id", None) or "").strip()
- row_creator = (getattr(row, "create_by", None) or "").strip()
- row_owner = (
- (getattr(row, owner_field, None) or "").strip() if owner_field else ""
- )
- if dept_ids and row_dept and row_dept in dept_ids:
- return True
- if user_ids and row_creator and row_creator in user_ids:
- return True
- if user_ids and row_owner and row_owner in user_ids:
- return True
- return False
- def assert_row_in_data_scope(
- db: Session,
- row,
- _model,
- user_info: dict,
- tenant_no: Optional[str] = None,
- owner_field: Optional[str] = None,
- ) -> None:
- """不在数据权限范围内时抛出 403。"""
- if not row_in_data_scope(db, row, user_info, tenant_no, owner_field=owner_field):
- raise HTTPException(status_code=403, detail="无权访问该数据")
|