支持具备 changeApplicant 权限时代填申请人及发票/余额联动;系统/CRM/BX/PL 主数据与下拉选项改为仅租户隔离,避免「仅本人」导致空选项。 Co-authored-by: Cursor <cursoragent@cursor.com>
285 lines
8.2 KiB
Python
285 lines
8.2 KiB
Python
"""数据权限过滤(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="无权访问该数据")
|