"""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()}