ForcePilot/backend/server/routers/external_systems/secret_rotation_router.py
Kris ba798a9bb5 feat(routers): add external systems management module and its sub routers
新增外部系统限界上下文的完整路由体系,包含聚合路由层与23个子领域路由,覆盖外部系统全生命周期管理、健康检查、指标监控、审计日志、Token管理、通知渠道、回收站等功能,并将外部系统路由挂载到全局API前缀下。
2026-06-20 22:16:07 +08:00

375 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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