- 平台治理: 租户用户权限层次、资源ACL、审批中心与审批模板、访问申请 - 存储: MinIO 存储进度迁移、对象存储安全加固与测试 - 计算: GPU 资源预留、compute 轮询与同步增强 - 权限: permission v2 迁移、权限安全验收测试 - 日志: 后端运行日志中文说明、操作日志整合 - 数据处理/评测: 数据转换与模型评测优化 Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
11 KiB
Python
327 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from fastapi import APIRouter, Body, Depends, Request
|
|
from typing import Any
|
|
|
|
from app.api.v1.endpoints.platform import ok, fail
|
|
from app.db.platform_store import get_platform_store
|
|
from app.core.auth import is_admin, require_admin, require_tenant_admin, get_current_user, user_tenant_ids
|
|
|
|
router = APIRouter(prefix="/tenants", tags=["tenant"])
|
|
|
|
|
|
def _actor(request: Request) -> str | None:
|
|
auth = request.headers.get("Authorization", "")
|
|
token = auth.replace("Bearer ", "").strip()
|
|
return token or None
|
|
|
|
|
|
@router.get("")
|
|
def list_tenants(current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
return ok(get_platform_store().tenants())
|
|
|
|
|
|
@router.get("/invitations")
|
|
def my_invitations(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
return ok(get_platform_store().tenant_invitations(str(current_user.get("id") or "")))
|
|
|
|
|
|
@router.get("/mine")
|
|
def my_tenants(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
return ok(get_platform_store().user_tenants(str(current_user.get("id") or ""), include_all=is_admin(current_user)))
|
|
|
|
|
|
@router.post("")
|
|
def create_tenant(payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
store = get_platform_store()
|
|
payload = {**payload, "owner_user_id": payload.get("owner_user_id") or current_user.get("id")}
|
|
try:
|
|
tenant = store.create_tenant(payload)
|
|
except KeyError as e:
|
|
raise fail(400, f"missing field: {e}")
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
store.record_audit(
|
|
action="tenant.create",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant",
|
|
target_id=tenant["id"],
|
|
tenant_id=tenant["id"],
|
|
detail=f"name={tenant.get('name')}",
|
|
)
|
|
return ok(tenant)
|
|
|
|
|
|
@router.get("/{tenant_id}")
|
|
def get_tenant(tenant_id: str, current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
try:
|
|
return ok(get_platform_store().tenant(tenant_id))
|
|
except KeyError:
|
|
raise fail(404, "tenant not found")
|
|
|
|
|
|
@router.put("/{tenant_id}")
|
|
def update_tenant(tenant_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
store = get_platform_store()
|
|
try:
|
|
tenant = store.update_tenant(tenant_id, payload)
|
|
except KeyError:
|
|
raise fail(404, "tenant not found")
|
|
store.record_audit(
|
|
action="tenant.update",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant",
|
|
target_id=tenant_id,
|
|
tenant_id=tenant_id,
|
|
detail=f"fields={','.join(payload.keys())}",
|
|
)
|
|
return ok(tenant)
|
|
|
|
|
|
@router.put("/{tenant_id}/quota")
|
|
def set_quota(tenant_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
store = get_platform_store()
|
|
try:
|
|
tenant = store.set_tenant_quota(tenant_id, payload)
|
|
except KeyError:
|
|
raise fail(404, "tenant not found")
|
|
store.record_audit(
|
|
action="tenant.quota.set",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant",
|
|
target_id=tenant_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
return ok(tenant)
|
|
|
|
|
|
@router.put("/{tenant_id}/retention-policy")
|
|
def set_retention(tenant_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
store = get_platform_store()
|
|
try:
|
|
tenant = store.set_tenant_retention(tenant_id, payload.get("retention_policy_id"))
|
|
except KeyError:
|
|
raise fail(404, "tenant not found")
|
|
store.record_audit(
|
|
action="tenant.retention.set",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant",
|
|
target_id=tenant_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
return ok(tenant)
|
|
|
|
|
|
@router.delete("/{tenant_id}")
|
|
def delete_tenant(tenant_id: str, request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
store = get_platform_store()
|
|
try:
|
|
tenant = store.delete_tenant(tenant_id, str(current_user.get("id") or "system"))
|
|
except KeyError:
|
|
raise fail(404, "tenant not found")
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
store.record_audit(
|
|
action="tenant.delete",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant",
|
|
target_id=tenant_id,
|
|
tenant_id=tenant_id,
|
|
detail=f"name={tenant.get('name')}",
|
|
)
|
|
return ok(tenant)
|
|
|
|
|
|
@router.post("/{tenant_id}/restore")
|
|
def restore_tenant(tenant_id: str, request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]:
|
|
store = get_platform_store()
|
|
try:
|
|
tenant = store.restore_tenant(tenant_id, str(current_user.get("id") or "system"))
|
|
except KeyError:
|
|
raise fail(404, "tenant not found")
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
store.record_audit(
|
|
action="tenant.restore",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant",
|
|
target_id=tenant_id,
|
|
tenant_id=tenant_id,
|
|
detail=f"name={tenant.get('name')}",
|
|
)
|
|
return ok(tenant)
|
|
|
|
|
|
@router.get("/{tenant_id}/quota/usage")
|
|
def quota_usage(tenant_id: str, current_user: dict = Depends(require_tenant_admin)) -> dict[str, Any]:
|
|
return ok(get_platform_store().tenant_quota_usage(tenant_id))
|
|
|
|
|
|
@router.post("/{tenant_id}/quota/request")
|
|
def request_quota_change(tenant_id: str, payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
if not is_admin(current_user) and tenant_id not in user_tenant_ids(current_user):
|
|
raise fail(403, "tenant access denied")
|
|
try:
|
|
get_platform_store().assert_active_tenant(tenant_id)
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
quota = payload.get("quota")
|
|
if not isinstance(quota, dict):
|
|
raise fail(400, "quota must be an object")
|
|
instance = get_platform_store().create_approval_instance({
|
|
"resource_type": "tenant",
|
|
"resource_id": tenant_id,
|
|
"applicant_id": current_user.get("id"),
|
|
"action": "tenant.quota.update",
|
|
"tenant_id": tenant_id,
|
|
"reason": json.dumps({"quota": quota}, ensure_ascii=False),
|
|
})
|
|
get_platform_store().record_audit(
|
|
action="tenant.quota.request", actor_id=current_user.get("id"),
|
|
target_type="tenant", target_id=tenant_id, tenant_id=tenant_id,
|
|
)
|
|
return ok({"approval_required": True, "approval_id": instance["id"], "approval": instance})
|
|
|
|
|
|
@router.get("/{tenant_id}/members")
|
|
def list_members(tenant_id: str, current_user: dict = Depends(require_tenant_admin)) -> dict[str, Any]:
|
|
try:
|
|
return ok(get_platform_store().tenant_members(tenant_id))
|
|
except KeyError:
|
|
raise fail(404, "tenant not found")
|
|
|
|
|
|
@router.post("/{tenant_id}/members")
|
|
def add_member(
|
|
tenant_id: str,
|
|
payload: dict[str, Any] = Body(...),
|
|
request: Request = None,
|
|
current_user: dict = Depends(require_tenant_admin),
|
|
) -> dict[str, Any]:
|
|
if not payload.get("user_id"):
|
|
raise fail(400, "user_id 必填")
|
|
if not is_admin(current_user) and payload.get("role") == "owner":
|
|
raise fail(403, "only platform administrator can grant owner role")
|
|
try:
|
|
member = get_platform_store().add_tenant_member(
|
|
tenant_id,
|
|
str(payload["user_id"]),
|
|
str(payload.get("role") or "member"),
|
|
current_user.get("id"),
|
|
)
|
|
except KeyError:
|
|
raise fail(404, "tenant or user not found")
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
get_platform_store().record_audit(
|
|
action="tenant.member.add",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant_member",
|
|
target_id=f"{tenant_id}:{payload['user_id']}",
|
|
tenant_id=tenant_id,
|
|
)
|
|
return ok(member)
|
|
|
|
|
|
@router.post("/{tenant_id}/members/invite")
|
|
def invite_member(
|
|
tenant_id: str,
|
|
payload: dict[str, Any] = Body(...),
|
|
current_user: dict = Depends(require_tenant_admin),
|
|
) -> dict[str, Any]:
|
|
user_id = str(payload.get("user_id") or "")
|
|
if not user_id:
|
|
raise fail(400, "user_id 必填")
|
|
if payload.get("role") == "owner":
|
|
raise fail(403, "tenant invitations cannot grant owner role")
|
|
try:
|
|
member = get_platform_store().invite_tenant_member(
|
|
tenant_id,
|
|
user_id,
|
|
str(payload.get("role") or "member"),
|
|
current_user.get("id"),
|
|
payload.get("expires_at"),
|
|
)
|
|
except KeyError:
|
|
raise fail(404, "tenant or active user not found")
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
get_platform_store().record_audit(
|
|
action="tenant.member.invite",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant_member",
|
|
target_id=f"{tenant_id}:{user_id}",
|
|
tenant_id=tenant_id,
|
|
detail=f"role={member.get('role')};expires_at={member.get('expires_at')}",
|
|
)
|
|
return ok(member)
|
|
|
|
|
|
@router.put("/{tenant_id}/members/{user_id}")
|
|
def update_member(
|
|
tenant_id: str,
|
|
user_id: str,
|
|
payload: dict[str, Any] = Body(...),
|
|
current_user: dict = Depends(require_tenant_admin),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
if not is_admin(current_user) and payload.get("role") == "owner":
|
|
raise fail(403, "only platform administrator can grant owner role")
|
|
member = get_platform_store().update_tenant_member(tenant_id, user_id, payload)
|
|
get_platform_store().record_audit(
|
|
action="tenant.member.update",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant_member",
|
|
target_id=f"{tenant_id}:{user_id}",
|
|
tenant_id=tenant_id,
|
|
detail=f"fields={','.join(payload.keys())}",
|
|
)
|
|
return ok(member)
|
|
except KeyError:
|
|
raise fail(404, "tenant member not found")
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
|
|
|
|
@router.delete("/{tenant_id}/members/{user_id}")
|
|
def remove_member(tenant_id: str, user_id: str, current_user: dict = Depends(require_tenant_admin)) -> dict[str, Any]:
|
|
try:
|
|
get_platform_store().remove_tenant_member(tenant_id, user_id)
|
|
except KeyError:
|
|
raise fail(404, "tenant member not found")
|
|
except ValueError as exc:
|
|
raise fail(400, str(exc))
|
|
get_platform_store().record_audit(
|
|
action="tenant.member.remove",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant_member",
|
|
target_id=f"{tenant_id}:{user_id}",
|
|
tenant_id=tenant_id,
|
|
)
|
|
return ok({"tenant_id": tenant_id, "user_id": user_id, "removed": True})
|
|
|
|
|
|
@router.post("/{tenant_id}/members/{user_id}/accept")
|
|
def accept_invitation(
|
|
tenant_id: str,
|
|
user_id: str,
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
if not is_admin(current_user) and str(current_user.get("id") or "") != user_id:
|
|
raise fail(403, "only the invited user can accept this invitation")
|
|
try:
|
|
member = get_platform_store().accept_tenant_invitation(tenant_id, user_id)
|
|
except KeyError:
|
|
raise fail(404, "tenant invitation not found")
|
|
except ValueError as exc:
|
|
raise fail(409, str(exc))
|
|
get_platform_store().record_audit(
|
|
action="tenant.member.accept",
|
|
actor_id=current_user.get("id"),
|
|
target_type="tenant_member",
|
|
target_id=f"{tenant_id}:{user_id}",
|
|
tenant_id=tenant_id,
|
|
)
|
|
return ok(member)
|