新增外部系统限界上下文的完整路由体系,包含聚合路由层与23个子领域路由,覆盖外部系统全生命周期管理、健康检查、指标监控、审计日志、Token管理、通知渠道、回收站等功能,并将外部系统路由挂载到全局API前缀下。
375 lines
14 KiB
Python
375 lines
14 KiB
Python
"""SecretRotation 子域 Router。
|
||
|
||
外部系统限界上下文的密钥轮换策略管理 API,覆盖策略 CRUD、单策略即时轮换、
|
||
轮换预检、通知配置管理、状态重置、统计聚合、到期/过期/即将到期查询、批量克隆。
|
||
所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,经
|
||
``secret_rotation_service`` 端口调用用例。
|
||
|
||
Request Schema 与 Input DTO 不共享类,Router 内显式构造 DTO,操作人字段
|
||
(``created_by`` / ``updated_by`` / ``operated_by``)由 ``current_user.uid`` 填充。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, Query
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
|
||
from yuxi.external_systems.use_cases.dto.secret_rotation import (
|
||
BatchClonePoliciesInput,
|
||
CreateSecretRotationPolicyInput,
|
||
DeletePolicyInput,
|
||
GetNotifyConfigInput,
|
||
GetPolicyInput,
|
||
ListDuePoliciesInput,
|
||
ListExpiringPoliciesInput,
|
||
ListOverduePoliciesInput,
|
||
ListSecretRotationPoliciesInput,
|
||
PrecheckRotationInput,
|
||
ResetPolicyInput,
|
||
RotatePolicyInput,
|
||
SecretRotationStatsInput,
|
||
UpdateNotifyConfigInput,
|
||
UpdatePolicyInput,
|
||
)
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||
|
||
secret_rotation_router = APIRouter(
|
||
prefix="/secret-rotations",
|
||
tags=["external-systems-secret-rotation"],
|
||
)
|
||
|
||
|
||
# ---------------- Request Schemas ----------------
|
||
|
||
|
||
class CreateSecretRotationPolicyRequest(BaseModel):
|
||
"""创建密钥轮换策略请求体。字段对齐 ``CreateSecretRotationPolicyInput``(不含 created_by)。
|
||
|
||
``created_by`` 由 Router 从 ``current_user.uid`` 注入。字段长度约束对齐
|
||
``ExternalSecretRotationPolicy`` ORM 列定义,在边界层拦截非法输入。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
system_id: int
|
||
secret_type: str = Field(..., min_length=1, max_length=32)
|
||
secret_name: str = Field(..., min_length=1, max_length=128)
|
||
secret_ref: str = Field(..., min_length=1, max_length=128)
|
||
env_key: str = Field(default="default", max_length=32)
|
||
rotation_mode: str = Field(default="manual", max_length=32)
|
||
rotation_interval_days: int = Field(default=90, ge=1)
|
||
notify_before_days: int = Field(default=7, ge=0)
|
||
notify_channels: dict[str, Any] = Field(default_factory=dict)
|
||
max_rotation_retries: int = Field(default=3, ge=0)
|
||
|
||
|
||
class UpdateSecretRotationPolicyRequest(BaseModel):
|
||
"""更新密钥轮换策略请求体。``id`` 由路径提供,``updated_by`` 由认证用户填充。
|
||
|
||
所有字段均为可选,仅透传客户端显式设置的字段。字符串字段长度约束对齐
|
||
``ExternalSecretRotationPolicy`` ORM 列定义。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
rotation_mode: str | None = Field(default=None, max_length=32)
|
||
rotation_interval_days: int | None = Field(default=None, ge=1)
|
||
notify_before_days: int | None = Field(default=None, ge=0)
|
||
notify_channels: dict[str, Any] | None = None
|
||
max_rotation_retries: int | None = Field(default=None, ge=0)
|
||
status: str | None = Field(default=None, max_length=32)
|
||
|
||
|
||
class RotatePolicyRequest(BaseModel):
|
||
"""立即执行轮换请求体。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
new_secret: str | None = None
|
||
|
||
|
||
class UpdateNotifyConfigRequest(BaseModel):
|
||
"""更新通知配置请求体。字段约束对齐 ``ExternalSecretRotationPolicy`` ORM 列定义。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
notify_channels: dict[str, Any] = Field(default_factory=dict)
|
||
notify_before_days: int = Field(default=7, ge=0)
|
||
|
||
|
||
class ResetPolicyRequest(BaseModel):
|
||
"""重置策略状态请求体。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
reason: str | None = None
|
||
|
||
|
||
class BatchClonePoliciesRequest(BaseModel):
|
||
"""批量克隆策略请求体。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
source_policy_ids: list[int]
|
||
override_config: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
# ---------------- /policies 端点 ----------------
|
||
|
||
|
||
@secret_rotation_router.get("/policies", response_model=dict)
|
||
async def list_policies(
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
system_id: int | None = Query(None),
|
||
env_key: str | None = Query(None),
|
||
secret_type: str | None = Query(None),
|
||
status: str | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""分页列出密钥轮换策略。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListSecretRotationPoliciesInput(
|
||
page=offset // limit + 1,
|
||
page_size=limit,
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
secret_type=secret_type,
|
||
status=status,
|
||
)
|
||
output = await use_cases.secret_rotation_service.list_policies(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.post("/policies", response_model=dict)
|
||
async def create_policy(
|
||
payload: CreateSecretRotationPolicyRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""创建密钥轮换策略。``created_by`` 由当前管理员填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = CreateSecretRotationPolicyInput(
|
||
**payload.model_dump(),
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.secret_rotation_service.create_policy(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# ---------------- /policies 静态路径端点(须在 /{policy_id} 之前声明) ----------------
|
||
|
||
|
||
@secret_rotation_router.get("/policies/stats", response_model=dict)
|
||
async def get_stats(
|
||
system_id: int | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询策略统计聚合。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = SecretRotationStatsInput(system_id=system_id)
|
||
output = await use_cases.secret_rotation_service.get_stats(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.get("/policies/due", response_model=dict)
|
||
async def list_due_policies(
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询到期策略。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListDuePoliciesInput(page=offset // limit + 1, page_size=limit)
|
||
output = await use_cases.secret_rotation_service.list_due_policies(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.get("/policies/overdue", response_model=dict)
|
||
async def list_overdue_policies(
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询过期策略。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListOverduePoliciesInput(page=offset // limit + 1, page_size=limit)
|
||
output = await use_cases.secret_rotation_service.list_overdue_policies(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.get("/policies/expiring", response_model=dict)
|
||
async def list_expiring_policies(
|
||
notify_before_days: int = Query(7, ge=0),
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询即将到期需通知的策略。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListExpiringPoliciesInput(
|
||
notify_before_days=notify_before_days,
|
||
page=offset // limit + 1,
|
||
page_size=limit,
|
||
)
|
||
output = await use_cases.secret_rotation_service.list_expiring_policies(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.post("/policies/batch-clone", response_model=dict)
|
||
async def batch_clone_policies(
|
||
payload: BatchClonePoliciesRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""批量克隆密钥轮换策略。``created_by`` 由当前管理员填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = BatchClonePoliciesInput(
|
||
source_policy_ids=payload.source_policy_ids,
|
||
override_config=payload.override_config,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.secret_rotation_service.batch_clone_policies(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# ---------------- /policies/{policy_id} 端点 ----------------
|
||
|
||
|
||
@secret_rotation_router.get("/policies/{policy_id}", response_model=dict)
|
||
async def get_policy(
|
||
policy_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取密钥轮换策略详情。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetPolicyInput(id=policy_id)
|
||
output = await use_cases.secret_rotation_service.get_policy(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.put("/policies/{policy_id}", response_model=dict)
|
||
async def update_policy(
|
||
policy_id: int,
|
||
payload: UpdateSecretRotationPolicyRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""更新密钥轮换策略。``updated_by`` 由当前管理员填充。仅透传客户端显式设置的字段。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = UpdatePolicyInput(
|
||
id=policy_id,
|
||
**payload.model_dump(exclude_unset=True),
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.secret_rotation_service.update_policy(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.delete("/policies/{policy_id}", response_model=dict)
|
||
async def delete_policy(
|
||
policy_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""删除密钥轮换策略。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = DeletePolicyInput(id=policy_id)
|
||
output = await use_cases.secret_rotation_service.delete_policy(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# ---------------- /policies/{policy_id} 子资源端点 ----------------
|
||
|
||
|
||
@secret_rotation_router.post("/policies/{policy_id}/execute", response_model=dict)
|
||
async def execute_rotation(
|
||
policy_id: int,
|
||
payload: RotatePolicyRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""立即执行密钥轮换。``operated_by`` 由当前管理员填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = RotatePolicyInput(
|
||
policy_id=policy_id,
|
||
new_secret=payload.new_secret,
|
||
operated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.secret_rotation_service.rotate_policy(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.post("/policies/{policy_id}/precheck", response_model=dict)
|
||
async def precheck_rotation(
|
||
policy_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""轮换预检(只读查询,不执行轮换)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = PrecheckRotationInput(policy_id=policy_id)
|
||
output = await use_cases.secret_rotation_service.precheck_rotation(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.get("/policies/{policy_id}/notify-config", response_model=dict)
|
||
async def get_notify_config(
|
||
policy_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询通知配置。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetNotifyConfigInput(policy_id=policy_id)
|
||
output = await use_cases.secret_rotation_service.get_notify_config(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.put("/policies/{policy_id}/notify-config", response_model=dict)
|
||
async def update_notify_config(
|
||
policy_id: int,
|
||
payload: UpdateNotifyConfigRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""更新通知配置。``updated_by`` 由当前管理员填充。覆盖式更新 notify_channels + notify_before_days。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = UpdateNotifyConfigInput(
|
||
policy_id=policy_id,
|
||
notify_channels=payload.notify_channels,
|
||
notify_before_days=payload.notify_before_days,
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.secret_rotation_service.update_notify_config(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@secret_rotation_router.post("/policies/{policy_id}/reset", response_model=dict)
|
||
async def reset_policy(
|
||
policy_id: int,
|
||
payload: ResetPolicyRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""重置策略状态。``operated_by`` 由当前管理员填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ResetPolicyInput(
|
||
policy_id=policy_id,
|
||
reason=payload.reason,
|
||
operated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.secret_rotation_service.reset_policy(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|