1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
405 lines
15 KiB
Python
405 lines
15 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,
|
||
TogglePolicyStatusInput,
|
||
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", 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"),
|
||
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()}
|
||
|
||
|
||
@secret_rotation_router.post("/policies/{policy_id}/toggle-status", response_model=dict)
|
||
async def toggle_policy_status(
|
||
policy_id: int,
|
||
payload: TogglePolicyStatusRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""切换策略状态(active <-> disabled)。``operated_by`` 由当前管理员填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
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()}
|