本次提交对多个外部系统路由进行了多维度优化: 1. 统一分页参数:将所有路由的`page = offset//limit +1`、`page_size=limit`替换为标准的`limit`+`offset`分页格式 2. 完善接口文档:补充多个端点的功能说明、参数含义与返回字段解释 3. 增强参数校验:新增字段长度限制、正则校验、枚举类型约束与业务逻辑校验 4. 优化代码复用:提取重复逻辑为辅助函数,减少样板代码 5. 修复接口问题:修正工具健康检查端点路径参数类型,优化导出接口响应格式 6. 补充异常处理:为批量操作添加异常捕获与日志记录,避免流程中断
378 lines
14 KiB
Python
378 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, Literal
|
||
|
||
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: Literal["client_secret", "api_key", "cert", "password", "connection_string"]
|
||
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: Literal["auto", "manual"] = "manual"
|
||
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`` 由认证用户填充。
|
||
|
||
所有字段均为可选,仅透传客户端显式设置的字段。``status`` 不在此暴露,
|
||
状态变更只能通过 ``/reset``、``/execute`` 等专用端点,避免绕过状态机。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
rotation_mode: Literal["auto", "manual"] | None = None
|
||
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)
|
||
|
||
|
||
class RotatePolicyRequest(BaseModel):
|
||
"""立即执行轮换请求体。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
new_secret: str | None = Field(default=None, min_length=1, max_length=4096)
|
||
|
||
|
||
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] = Field(..., min_length=1, max_length=100)
|
||
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),
|
||
rotation_mode: Literal["auto", "manual"] | None = Query(None),
|
||
status: Literal["active", "rotating", "overdue", "failed", "disabled"] | None = Query(None),
|
||
keyword: str | None = Query(None, max_length=128, description="搜索 secret_name/secret_ref"),
|
||
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(
|
||
limit=limit,
|
||
offset=offset,
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
secret_type=secret_type,
|
||
rotation_mode=rotation_mode,
|
||
status=status,
|
||
keyword=keyword,
|
||
)
|
||
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(limit=limit, offset=offset)
|
||
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(limit=limit, offset=offset)
|
||
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, deprecated=True, description="已废弃:使用策略自身的 notify_before_days"),
|
||
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,
|
||
limit=limit,
|
||
offset=offset,
|
||
)
|
||
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()}
|