2026-08-23 22:35:59 +08:00
|
|
|
# -*- coding: utf-8 -*-
|
2026-08-23 23:56:39 +08:00
|
|
|
"""接口层 RBAC 守卫工厂:角色 / 权限 / 数据范围守卫 + 审计写入。
|
2026-08-23 22:35:59 +08:00
|
|
|
|
2026-08-23 23:56:39 +08:00
|
|
|
判定语义在领域层 ``domain/rules.py``;本文件只把规则映射为 HTTP 异常。
|
2026-08-23 22:35:59 +08:00
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from collections.abc import Callable
|
|
|
|
|
|
|
|
|
|
from fastapi import Depends, HTTPException, Request
|
|
|
|
|
|
2026-08-23 23:52:58 +08:00
|
|
|
from .api.dependencies import get_current_user
|
2026-08-23 23:56:39 +08:00
|
|
|
from .domain.rules import (
|
|
|
|
|
is_government,
|
|
|
|
|
permission_granted,
|
|
|
|
|
region_in_scope,
|
|
|
|
|
role_allowed,
|
|
|
|
|
scope_level_ok,
|
|
|
|
|
sub_role_allowed,
|
|
|
|
|
)
|
2026-08-23 23:52:58 +08:00
|
|
|
from .infrastructure.repositories import Database
|
2026-08-23 22:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_roles(*roles: str) -> Callable:
|
|
|
|
|
"""要求当前用户业务角色 ∈ roles;否则 403。"""
|
|
|
|
|
def dep(user: dict = Depends(get_current_user)) -> dict:
|
2026-08-23 23:56:39 +08:00
|
|
|
if not role_allowed(user, *roles):
|
2026-08-23 22:35:59 +08:00
|
|
|
raise HTTPException(status_code=403, detail="Forbidden: insufficient role")
|
|
|
|
|
return user
|
|
|
|
|
return dep
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_sub_roles(*sub_roles: str) -> Callable:
|
|
|
|
|
"""要求当前用户子角色 ∈ sub_roles(如 op_super_admin);否则 403。"""
|
|
|
|
|
def dep(user: dict = Depends(get_current_user)) -> dict:
|
2026-08-23 23:56:39 +08:00
|
|
|
if not sub_role_allowed(user, *sub_roles):
|
2026-08-23 22:35:59 +08:00
|
|
|
raise HTTPException(status_code=403, detail="Forbidden: insufficient sub-role")
|
|
|
|
|
return user
|
|
|
|
|
return dep
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_permission(perm: str) -> Callable:
|
|
|
|
|
"""要求当前用户拥有权限码 ``perm``;否则 403。"""
|
|
|
|
|
def dep(user: dict = Depends(get_current_user)) -> dict:
|
2026-08-23 23:56:39 +08:00
|
|
|
if not permission_granted(user, perm):
|
2026-08-23 22:35:59 +08:00
|
|
|
raise HTTPException(status_code=403, detail=f"Forbidden: missing permission {perm}")
|
|
|
|
|
return user
|
|
|
|
|
return dep
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_scope(min_level: str | None = None) -> Callable:
|
2026-08-23 23:56:39 +08:00
|
|
|
"""政务数据范围守卫:要求 government 且区域层级不低于 min_level。"""
|
2026-08-23 22:35:59 +08:00
|
|
|
def dep(user: dict = Depends(get_current_user)) -> dict:
|
2026-08-23 23:56:39 +08:00
|
|
|
if not is_government(user):
|
2026-08-23 22:35:59 +08:00
|
|
|
raise HTTPException(status_code=403, detail="Forbidden: not government")
|
|
|
|
|
if not user.get("region_id"):
|
|
|
|
|
raise HTTPException(status_code=403, detail="Forbidden: no region scope")
|
2026-08-23 23:56:39 +08:00
|
|
|
if not scope_level_ok(user, min_level):
|
|
|
|
|
raise HTTPException(status_code=403, detail=f"Forbidden: scope below {min_level}")
|
2026-08-23 22:35:59 +08:00
|
|
|
return user
|
|
|
|
|
return dep
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def scope_covers(region_id: str | None) -> Callable:
|
|
|
|
|
"""要求 ``region_id`` 落在当前用户数据范围内;否则 403。"""
|
|
|
|
|
def dep(user: dict = Depends(get_current_user)) -> dict:
|
2026-08-23 23:56:39 +08:00
|
|
|
if not region_in_scope(user, region_id):
|
2026-08-23 22:35:59 +08:00
|
|
|
raise HTTPException(status_code=403, detail="Forbidden: out of data scope")
|
|
|
|
|
return user
|
|
|
|
|
return dep
|
|
|
|
|
|
|
|
|
|
|
2026-08-23 23:52:58 +08:00
|
|
|
async def write_audit(
|
2026-08-23 22:35:59 +08:00
|
|
|
db: Database,
|
|
|
|
|
*,
|
|
|
|
|
action: str,
|
|
|
|
|
resource: str,
|
|
|
|
|
resource_id: str = "",
|
|
|
|
|
detail: str = "",
|
|
|
|
|
user: dict | None = None,
|
|
|
|
|
request: Request | None = None,
|
|
|
|
|
) -> None:
|
2026-08-23 23:52:58 +08:00
|
|
|
"""写入一条审计日志(异步)。"""
|
2026-08-23 22:35:59 +08:00
|
|
|
ip = request.client.host if request is not None and request.client else ""
|
2026-08-23 23:52:58 +08:00
|
|
|
await db.audit.add(
|
2026-08-23 22:35:59 +08:00
|
|
|
action=action,
|
|
|
|
|
resource=resource,
|
|
|
|
|
resource_id=resource_id,
|
|
|
|
|
detail=detail,
|
|
|
|
|
ip=ip,
|
|
|
|
|
user_id=(user or {}).get("id"),
|
|
|
|
|
)
|