Files
server-core/app/rbac.py
T

93 lines
3.1 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2026-08-23 23:56:39 +08:00
"""接口层 RBAC 守卫工厂:角色 / 权限 / 数据范围守卫 + 审计写入。
2026-08-23 23:56:39 +08:00
判定语义在领域层 ``domain/rules.py``;本文件只把规则映射为 HTTP 异常。
"""
from __future__ import annotations
from collections.abc import Callable
from fastapi import Depends, HTTPException, Request
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,
)
from .infrastructure.repositories import Database
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):
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):
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):
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。"""
def dep(user: dict = Depends(get_current_user)) -> dict:
2026-08-23 23:56:39 +08:00
if not is_government(user):
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}")
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):
raise HTTPException(status_code=403, detail="Forbidden: out of data scope")
return user
return dep
async def write_audit(
db: Database,
*,
action: str,
resource: str,
resource_id: str = "",
detail: str = "",
user: dict | None = None,
request: Request | None = None,
) -> None:
"""写入一条审计日志(异步)。"""
ip = request.client.host if request is not None and request.client else ""
await db.audit.add(
action=action,
resource=resource,
resource_id=resource_id,
detail=detail,
ip=ip,
user_id=(user or {}).get("id"),
)