Files
group_fqcd_jr/app/api/controllers/rbac.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

85 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""角色与权限的只读接口(B1:先让管理员看得见)。
前缀与 `admin.py` 相同(`/api/v1/admin`),但**独立成文件**:`admin.py` 是表驱动的
配置面 CRUD 工厂(`RESOURCES` 单表增删改),而角色/权限是**多对多关系查询**,
塞进那个工厂既不自然、也会让"配置管理"和"身份管理"两件事混在一个文件里。
四个接口全部只读,且**不写审计**——它们返回的就是审计材料本身("谁能访问什么",
合规检查要看的正是这份清单)。`docs/05` §19 登记时"审计"列标"否"。
所有响应都走 `docs/05` §3.3 的统一信封(复用 `app/api/views/envelope.py`):
列表的 `data` 是纯数组、分页元数据在 `meta`;单对象的 `meta` 只有 `trace_id`。
"""
from fastapi import APIRouter, Depends, Path
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.auth import build_request_context
from app.api.dependencies.database import get_session
from app.api.dependencies.rate_limit import enforce_rate_limit
from app.api.views.envelope import envelope, list_envelope
from app.core.contracts import RequestContext
from app.service.rbac_query_service import RbacQueryService
router = APIRouter(
prefix="/api/v1/admin",
tags=["platform-admin"],
dependencies=[Depends(enforce_rate_limit)],
)
#: 角色码在 `sys_role` 里形如 `customer` / `risk_operator` / `admin`。
ROLE_CODE = Path(min_length=1, max_length=64, pattern=r"^[a-z0-9_]+$")
#: `sys_user.id` 是 BIGINT UNSIGNED。
USER_ID = Path(pattern=r"^[0-9]{1,20}$")
@router.get("/roles")
async def list_roles(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
"""所有角色 + 每个角色的权限数与在用人数。"""
page = await RbacQueryService(session).list_roles(context)
return list_envelope(page, context)
@router.get("/roles/{role_code}")
async def get_role(
role_code: str = ROLE_CODE,
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
"""单个角色的详情。
与 `/permissions` 分开是必要的:权限为空的角色(例如刚建好还没授权的)在一个合并
接口里很容易被"查不到权限"误判成"角色不存在"。
"""
data = await RbacQueryService(session).get_role_detail(context, role_code)
return envelope(data, context)
@router.get("/roles/{role_code}/permissions")
async def list_role_permissions(
role_code: str = ROLE_CODE,
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
"""该角色的权限清单,按权限码排序,便于与代码里各 Service 的 `require(...)` 对照。"""
page = await RbacQueryService(session).list_role_permissions(context, role_code)
return list_envelope(page, context)
@router.get("/users/{user_id}/roles")
async def get_user_roles(
user_id: str = USER_ID,
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
"""某个用户**实际解析出来**的角色 / 权限 / 数据范围 / 可见客户。
这是四个接口里最有用的一个:它直接回答"这个人为什么 403"——
走的是 `IdentityService.resolve`,与请求期完全同一条链路。
"""
data = await RbacQueryService(session).get_user_identity(context, user_id)
return envelope(data, context)