feat: harden data access and simplify user management

This commit is contained in:
xuelong committed 2026-08-25 21:02:30 -07:00
1 parent 3dd5731751
commit adb780bc82
51 files changed
+3239 -2121

No files matched your search

+44 -108
View File
@@ -1,135 +1,71 @@
"""RBAC & Accounts (P1) — roles, capability matrix, users CRUD."""
"""Simple user administration with two data-access levels."""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException
from app.auth import AdminUser
from app.contracts import (
CapabilityCreate,
RoleCapCell,
RoleCreate,
RoleUpdate,
UserCreate,
UserUpdate,
)
from app.db import (
create_capability,
create_role,
create_user,
delete_capability,
delete_role,
delete_user,
get_permission_matrix,
list_capabilities,
list_roles,
list_users,
set_role_cap,
update_role,
update_user,
)
from app.contracts import DataPermission, UserCreate, UserUpdate
from app.db import create_user, get_user_by_id, list_users, update_user
from app.security import hash_password
router = APIRouter()
# ── Roles ────────────────────────────────────────────────────────────────────
@router.get("/roles")
async def _list_roles(_user: AdminUser):
return await list_roles()
_PERMISSION_ROLES: dict[DataPermission, str] = {
"data_manage": "admin",
"data_view": "operator",
}
@router.post("/roles")
async def _create_role(body: RoleCreate, _user: AdminUser):
try:
return await create_role(body.model_dump())
except Exception as e: # unique violation etc.
raise HTTPException(400, f"创建角色失败:{str(e)[:200]}")
def _permission_from_roles(roles: list[str] | None) -> DataPermission:
return "data_manage" if "admin" in set(roles or []) else "data_view"
@router.patch("/roles/{role_key}")
async def _update_role(role_key: str, body: RoleUpdate, _user: AdminUser):
row = await update_role(role_key, body.model_dump(exclude_none=True))
if not row:
raise HTTPException(404, "角色不存在")
return row
def _public_user(row: dict[str, Any]) -> dict[str, Any]:
result = dict(row)
result["permission"] = _permission_from_roles(list(result.pop("roles", []) or []))
return result
@router.delete("/roles/{role_key}")
async def _delete_role(role_key: str, _user: AdminUser):
try:
ok = await delete_role(role_key)
except ValueError as e:
raise HTTPException(400, str(e))
if not ok:
raise HTTPException(404, "角色不存在")
return {"deleted": role_key}
# ── Capabilities ─────────────────────────────────────────────────────────────
@router.get("/capabilities")
async def _list_caps(_user: AdminUser):
return await list_capabilities()
@router.post("/capabilities")
async def _create_cap(body: CapabilityCreate, _user: AdminUser):
try:
return await create_capability(body.model_dump())
except Exception as e:
raise HTTPException(400, f"创建能力项失败:{str(e)[:200]}")
@router.delete("/capabilities/{cap_key}")
async def _delete_cap(cap_key: str, _user: AdminUser):
await delete_capability(cap_key)
return {"deleted": cap_key}
# ── Permission matrix ────────────────────────────────────────────────────────
@router.get("/permission-matrix")
async def _matrix(_user: AdminUser):
return await get_permission_matrix()
@router.put("/permission-matrix")
async def _set_cell(body: RoleCapCell, _user: AdminUser):
await set_role_cap(body.role_key, body.cap_key, body.value)
return {"ok": True, "role_key": body.role_key, "cap_key": body.cap_key, "value": body.value}
# ── Users ────────────────────────────────────────────────────────────────────
@router.get("/users")
async def _list_users(_user: AdminUser):
return await list_users()
return [_public_user(row) for row in await list_users()]
@router.post("/users")
async def _create_user(body: UserCreate, _user: AdminUser):
data = body.model_dump()
roles = data.pop("roles", [])
pw = data.pop("password")
data["hashed_password"] = hash_password(pw)
permission = data.pop("permission")
password = data.pop("password")
data["username"] = data["username"].strip()
data["hashed_password"] = hash_password(password)
try:
return await create_user(data, roles)
except Exception as e:
raise HTTPException(400, f"创建用户失败:{str(e)[:200]}")
row = await create_user(data, [_PERMISSION_ROLES[permission]])
except Exception as exc:
raise HTTPException(400, f"创建用户失败:{str(exc)[:200]}") from exc
return _public_user(row)
@router.patch("/users/{user_id}")
async def _update_user(user_id: int, body: UserUpdate, _user: AdminUser):
data = body.model_dump(exclude_none=True)
roles = data.pop("roles", None)
if "password" in data:
data["hashed_password"] = hash_password(data.pop("password"))
async def _update_user(user_id: int, body: UserUpdate, current_user: AdminUser):
target = await get_user_by_id(user_id)
if not target:
raise HTTPException(404, "用户不存在")
data = body.model_dump(exclude_unset=True)
permission = data.pop("permission", None)
is_current_user = target["username"] == current_user["username"]
if is_current_user and data.get("status") == "disabled":
raise HTTPException(400, "不能停用当前登录账号")
if is_current_user and permission == "data_view":
raise HTTPException(400, "不能移除当前账号的数据管理权限")
roles = [_PERMISSION_ROLES[permission]] if permission else None
password = data.pop("password", None)
if password:
data["hashed_password"] = hash_password(password)
row = await update_user(user_id, data, roles)
if not row:
raise HTTPException(404, "用户不存在")
return row
@router.delete("/users/{user_id}")
async def _delete_user(user_id: int, _user: AdminUser):
await delete_user(user_id)
return {"deleted": user_id}
return _public_user(row)