ForcePilot/backend/server/routers/external_systems/secret_rotation_router.py

375 lines
14 KiB
Python
Raw Normal View History

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