Files
server-core/app/api/routers/rbac_admin.py
T
Pine bc6265eb67 feat(opc): OPC认证/入园/转园 端点 + 园区端方案B审核 + 迁移0013
- rbac_opc.py: /opc/certification/apply|mine、/opc/park-transfer/apply|mine
- rbac_admin.py: platform operator 兜底 list/review (opc-certifications/park-admissions/park-transfers),通过回填 users.set_classification
- park/routers.py: carrier 角色凭 operator_user_id 定位本园区,/park/api park-admissions|transfers view+review
- park/tenants.py: bind_admin 写 operator_user_id + find_by_operator_user_id
- 迁移 0013: opc_certifications + park_transfers + park_tenants.operator_user_id
2026-08-26 15:43:34 +08:00

320 lines
13 KiB
Python

# -*- coding: utf-8 -*-
"""运营方管理端点(证明 RBAC 角色/权限/审计)。"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from ..dependencies import get_db
from ..schemas.admin import SetUserRoleRequest, SetUserStatusRequest, UserClassificationRequest
from ..schemas.admin import UserCreateRequest, RolePermissionRequest, UpdateUserRequest
from ..schemas.admin import OpcCertReviewRequest, ParkAdmissionReviewRequest, ParkTransferReviewRequest
from ...rbac import require_permission, require_roles, write_audit
from ...infrastructure.repositories import Database
from ...services import compute_client
router = APIRouter(prefix="/admin", tags=["admin"])
async def _sync_compute(coro) -> None:
"""算力引擎用户同步为附加能力:失败不阻断平台用户操作(引擎未配置/不可达时静默)。"""
try:
await coro
except Exception: # noqa: BLE001
pass
@router.post("/users", summary="新增账号(按类型/角色)")
async def create_user(
req: UserCreateRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.assign_role")),
):
from ...services.user_admin_service import UserAdminService
user = await UserAdminService(db).create_user(req, actor)
# 平台用户创建 → 引擎同步建号 + 签发 PAT(算力引擎用户与平台一致)
await _sync_compute(compute_client.create_user(user["username"]))
await _sync_compute(compute_client.issue_pat(user["username"]))
await write_audit(db, action="user.create", resource="user", resource_id=user["id"],
detail=f"role={req.role}", user=actor, request=request)
return await db.users.to_profile(user)
def _port_for_role(role: str) -> str:
return {
"opc_member": "opc", "carrier": "carrier", "enterprise": "enterprise",
"provider": "provider", "government": "government", "operator": "operator",
"investor": "investor", "developer": "developer",
}.get(role, "opc")
@router.get("/users", summary="用户列表(运营方)")
async def list_users(
request: Request,
db: Database = Depends(get_db),
_role: dict = Depends(require_roles("operator")),
_perm: dict = Depends(require_permission("menu:admin_user_mgmt")),
):
return await db.users.list()
@router.post("/users/{user_id}/role", summary="分配角色")
async def set_user_role(
user_id: str,
req: SetUserRoleRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.assign_role")),
):
from ...services.user_admin_service import UserAdminService
if await db.users.get_by_id(user_id) is None:
raise HTTPException(status_code=404, detail="User not found")
updated = await UserAdminService(db).set_user_role(
actor, user_id, req.role, req.sub_role, req.org_id, req.region_id,
)
await write_audit(
db, action="role.assign", resource="user", resource_id=user_id,
detail=f"{actor['username']} -> role={req.role} sub={req.sub_role}",
user=actor, request=request,
)
return {"ok": True, "user": await db.users.to_profile(updated)}
@router.post("/users/{user_id}/classification", summary="设置用户认证状态/所属/账号类型")
async def set_user_classification(
user_id: str,
req: UserClassificationRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.manage")),
):
fields = req.model_dump(exclude_none=True)
updated = await db.users.set_classification(user_id, **fields)
if updated is None:
raise HTTPException(status_code=404, detail="User not found")
await write_audit(db, action="user.classification", resource="user", resource_id=user_id,
detail=str(fields), user=actor, request=request)
return {"ok": True, "user": updated}
@router.post("/users/{user_id}/status", summary="禁用/启用用户")
async def set_user_status(
user_id: str,
req: SetUserStatusRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.disable")),
):
user = await db.users.get_by_id(user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
updated = await db.users.set_status(user_id, req.status)
# 平台用户启用/禁用 → 引擎镜像同步
await _sync_compute(compute_client.sync_user_enabled(
user.get("username"), req.status == "active",
))
await write_audit(
db, action="user.disable", resource="user", resource_id=user_id,
detail=f"{actor['username']} -> status={req.status}",
user=actor, request=request,
)
return {"ok": True, "user": await db.users.to_profile(updated)}
@router.put("/users/{user_id}", summary="修改用户(资料/角色)")
async def update_user(
user_id: str,
req: UpdateUserRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.manage")),
):
user = await db.users.get_by_id(user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
profile_fields = {k: v for k, v in req.model_dump(exclude_none=True).items() if k in ("nickname",)}
if profile_fields:
user = await db.users.update_profile(user_id, profile_fields) or user
if req.role or req.sub_role is not None:
user = await db.users.set_role(user_id, req.role or user.get("role"), req.sub_role,
req.org_id or user.get("org_id"), req.region_id or user.get("region_id"))
await write_audit(db, action="user.update", resource="user", resource_id=user_id,
detail=str(req.model_dump(exclude_none=True)), user=actor, request=request)
return {"ok": True, "user": await db.users.to_profile(user)}
@router.delete("/users/{user_id}", summary="删除用户")
async def delete_user(
user_id: str,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.manage")),
):
user = await db.users.get_by_id(user_id)
ok = await db.users.delete(user_id)
if not ok:
raise HTTPException(status_code=404, detail="User not found")
# 平台用户删除 → 引擎镜像同步删除
if user:
await _sync_compute(compute_client.sync_delete_user(user.get("username")))
await write_audit(db, action="user.delete", resource="user", resource_id=user_id,
detail="deleted", user=actor, request=request)
return {"ok": True, "user_id": user_id}
@router.get("/roles", summary="角色列表")
async def list_roles(
db: Database = Depends(get_db),
_user: dict = Depends(require_permission("menu:admin_role_mgmt")),
):
return await db.roles.list_roles()
@router.get("/permissions", summary="权限列表")
async def list_permissions(
db: Database = Depends(get_db),
_user: dict = Depends(require_permission("menu:admin_role_mgmt")),
):
return await db.roles.list_permissions()
@router.post("/roles/{role_id}/permissions", summary="配置角色权限")
async def set_role_permissions(
role_id: str,
req: RolePermissionRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:role.grant_perm")),
):
if not await db.roles.role_exists(role_id):
raise HTTPException(status_code=404, detail="Role not found")
perms = await db.roles.set_role_permissions(role_id, req.permissions)
await write_audit(db, action="role.grant_perm", resource="role", resource_id=role_id,
detail=f"permissions={len(req.permissions)}", user=actor, request=request)
return {"ok": True, "role_id": role_id, "permissions": perms}
@router.get("/audit-logs", summary="审计日志")
async def list_audit_logs(
limit: int = 100,
offset: int = 0,
db: Database = Depends(get_db),
_user: dict = Depends(require_permission("action:audit.view")),
):
return await db.audit.list(limit=min(limit, 500), offset=offset)
async def _apply_admission_user(db, adm, status):
"""入园/转园审核通过后回填 users 的园区归属。"""
if status not in ("approved", "certified"):
return
uid = (adm or {}).get("user_id")
if not uid:
return
await db.users.set_classification(uid, affiliation="park",
park_id=(adm or {}).get("tenant_id", ""),
park_name=(adm or {}).get("tenant_name", ""))
# ==================== OPC 认证 / 入园 / 转园 审核(平台 operator 兜底) ====================
@router.get("/opc-certifications", summary="OPC 认证申请列表(平台)")
async def list_opc_certifications(
status: str = "",
db: Database = Depends(get_db),
_role: dict = Depends(require_roles("operator")),
_perm: dict = Depends(require_permission("menu:admin_user_mgmt")),
):
return {"items": await db.opc_certifications.list(status or None)}
@router.post("/opc-certifications/{cert_id}/review", summary="审核 OPC 认证申请")
async def review_opc_certification(
cert_id: str,
req: OpcCertReviewRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.manage")),
):
cert = await db.opc_certifications.get(cert_id)
if cert is None:
raise HTTPException(status_code=404, detail="认证申请不存在")
if req.status not in ("certified", "rejected"):
raise HTTPException(status_code=400, detail="状态仅支持 certified/rejected")
updated = await db.opc_certifications.set_status(cert_id, req.status,
reviewer=actor.get("id", ""), comment=req.comment)
if cert.get("user_id"):
u = await db.users.get_by_id(cert["user_id"])
await db.users.set_classification(cert["user_id"],
certification_status=req.status, affiliation=(u or {}).get("affiliation") or "independent")
await write_audit(db, action="opc.cert_review", resource="opc_certification",
resource_id=cert_id, detail=f"status={req.status}", user=actor, request=request)
return {"ok": True, "certification": updated}
@router.get("/park-admissions", summary="园区入驻申请列表(平台)")
async def list_park_admissions(
status: str = "",
db: Database = Depends(get_db),
_role: dict = Depends(require_roles("operator")),
_perm: dict = Depends(require_permission("menu:admin_user_mgmt")),
):
return {"items": await db.park_admissions.list(status or None)}
@router.post("/park-admissions/{aid}/review", summary="园区入驻申请审核(平台兜底)")
async def review_park_admission(
aid: str,
req: ParkAdmissionReviewRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.manage")),
):
adm = await db.park_admissions.get(aid)
if adm is None:
raise HTTPException(status_code=404, detail="入驻申请不存在")
if req.status not in ("approved", "rejected", "reviewing"):
raise HTTPException(status_code=400, detail="状态仅支持 approved/rejected/reviewing")
updated = await db.park_admissions.set_status(aid, req.status,
reviewer=actor.get("id", ""), comment=req.comment)
await _apply_admission_user(db, adm, req.status)
await write_audit(db, action="opc.admission_review", resource="park_admission",
resource_id=aid, detail=f"status={req.status}", user=actor, request=request)
return {"ok": True, "admission": updated}
@router.get("/park-transfers", summary="OPC 转园申请列表(平台)")
async def list_park_transfers(
status: str = "",
db: Database = Depends(get_db),
_role: dict = Depends(require_roles("operator")),
_perm: dict = Depends(require_permission("menu:admin_user_mgmt")),
):
return {"items": await db.park_transfers.list(status or None)}
@router.post("/park-transfers/{tid}/review", summary="OPC 转园申请审核(平台兜底)")
async def review_park_transfer(
tid: str,
req: ParkTransferReviewRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:user.manage")),
):
tr = await db.park_transfers.get(tid)
if tr is None:
raise HTTPException(status_code=404, detail="转园申请不存在")
if req.status not in ("approved", "rejected", "reviewing"):
raise HTTPException(status_code=400, detail="状态仅支持 approved/rejected/reviewing")
updated = await db.park_transfers.set_status(tid, req.status,
reviewer=actor.get("id", ""), comment=req.comment)
if req.status == "approved":
await db.users.set_classification(tr["user_id"], affiliation="park",
park_id=tr.get("to_park_id", ""), park_name=tr.get("to_park_name", ""))
await write_audit(db, action="opc.park_transfer_review", resource="park_transfer",
resource_id=tid, detail=f"status={req.status}", user=actor, request=request)
return {"ok": True, "transfer": updated}