ForcePilot/backend/server/routers/external_systems/secret_rotation_router.py
Kris 002e6a356b chore: 批量整理代码变更,修复多类细节问题
1.  清理测试文件中未使用的导入与冗余代码
2.  修复审计日志与批量操作的空值约束,统一填充"global"作为默认渠道
3.  调整批量消息撤回的响应语义,对齐其他端点的部分成功契约
4.  修复访问规则批量克隆的唯一约束问题,新增后缀自动处理逻辑
5.  替换anyio为asyncio并行调用,修正时间UTC导入路径
6.  优化前端外部系统概览页的刷新状态提示与缓存逻辑
7.  修复测试用例中的断言与请求方式问题,适配httpx删除请求特性
8.  重构后端路由的依赖注入,移除冗余的数据库会话依赖
9.  调整测试用例的权限校验逻辑,修正强制登出的权限判断
10. 修复语义分块测试的numpy依赖问题,清理冗余导入
2026-07-13 20:48:29 +08:00

389 lines
15 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, Literal
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
from yuxi.external_systems.infrastructure.container import UseCases
from yuxi.external_systems.use_cases.dto.secret_rotation import (
BatchClonePoliciesInput,
CreateSecretRotationPolicyInput,
DeletePolicyInput,
GetNotifyConfigInput,
GetPolicyInput,
ListDuePoliciesInput,
ListExpiringPoliciesInput,
ListOverduePoliciesInput,
ListSecretRotationPoliciesInput,
PrecheckRotationInput,
ResetPolicyInput,
RotatePolicyInput,
SecretRotationStatsInput,
TogglePolicyStatusInput,
UpdateNotifyConfigInput,
UpdatePolicyInput,
)
from yuxi.storage.postgres.models_business import User
from server.routers.external_systems import get_use_cases
from server.utils.auth_middleware import get_admin_user, 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", min_length=1, 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] = Field(default_factory=dict)
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 = Field(default=None, max_length=512)
class TogglePolicyStatusRequest(BaseModel):
"""切换策略状态请求体active <-> disabled"""
model_config = ConfigDict(frozen=True)
status: Literal["active", "disabled"]
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, max_length=32),
secret_type: Literal["client_secret", "api_key", "cert", "password", "connection_string"] | 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"),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""分页列出密钥轮换策略(支持过滤 + 关键词搜索)。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""创建密钥轮换策略。``created_by`` 由当前管理员填充。"""
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),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询策略统计聚合。"""
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),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询到期策略。"""
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),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询过期策略。"""
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),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询即将到期需通知的策略。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量克隆密钥轮换策略。``created_by`` 由当前管理员填充。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""获取密钥轮换策略详情。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新密钥轮换策略。``updated_by`` 由当前管理员填充。仅透传客户端显式设置的字段。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""删除密钥轮换策略。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""立即执行密钥轮换。``operated_by`` 由当前管理员填充。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""轮换预检(只读查询,不执行轮换)。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询通知配置。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新通知配置。``updated_by`` 由当前管理员填充。覆盖式更新 notify_channels + notify_before_days。"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""重置策略状态。``operated_by`` 由当前管理员填充。"""
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()}
@secret_rotation_router.post("/policies/{policy_id}/toggle-status", response_model=dict)
async def toggle_policy_status(
policy_id: int,
payload: TogglePolicyStatusRequest,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""切换策略状态active <-> disabled。``operated_by`` 由当前管理员填充。"""
input_dto = TogglePolicyStatusInput(
policy_id=policy_id,
status=payload.status,
operated_by=current_user.uid,
)
output = await use_cases.secret_rotation_service.toggle_policy_status(input_dto)
return {"success": True, "data": output.model_dump()}