ForcePilot/backend/server/routers/external_systems/auth_plugin_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

316 lines
12 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.

"""AuthPlugin 子域 Router。
外部系统限界上下文的认证插件查询 API。所有端点直接读取 ``auth_plugins`` 注册表
全局单例(框架级 + 协议专属)与 ``token_refresh_registry`` 全局单例,不引入
use_cases 层——数据源为代码注册的内存对象,无持久化需求,与 adapter_router
元数据端点保持一致。
设计依据docs/vibe/v1.1/设计方案/RouterAPI扩展设计/11-auth_plugin_router扩展设计方案.md
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from yuxi.external_systems.exceptions import AuthError, SecretResolutionError
from yuxi.external_systems.framework.auth_plugins import (
AUTH_PLUGINS,
get_auth_json_schema,
get_auth_plugin,
get_protocol_auth_plugin,
is_token_based_auth_type,
list_all_protocol_auth_plugins,
list_auth_plugins,
resolve_credential,
)
from yuxi.external_systems.framework.runtime.token_refresh_registry import (
token_refresh_registry,
)
from yuxi.storage.postgres.models_business import User
from server.utils.auth_middleware import get_admin_user, get_required_user
# =============================================================================
# === Request Schemas ===
# =============================================================================
class ValidateAuthConfigRequest(BaseModel):
"""认证配置校验请求体。"""
auth_config: dict[str, Any]
auth_plugin_router = APIRouter(
prefix="/auth-plugins",
tags=["external-systems-auth-plugin"],
)
# =============================================================================
# === 静态路径端点(必须在 /{auth_type} 之前声明) ===
# =============================================================================
@auth_plugin_router.get("", response_model=dict)
async def list_auth_plugins_all(
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询系统中所有认证插件(框架级 + 所有协议专属),返回扁平列表。
框架级条目(``is_framework=True``、``adapter_type=None``)在前,协议专属
条目(``is_framework=False``、``adapter_type=str``)在后。每个条目额外
标记 ``is_token_based`` 与 ``has_refresh_strategy``,便于前端区分静态凭证、
Token 类无刷新、Token 类有刷新三类认证。
与 ``GET /auth-plugins/overview`` 的差异:本端点返回扁平列表(适合表格展示),
overview 端点返回分组结构(适合树形展示)。
"""
framework_plugins = list_auth_plugins(None)
protocol_plugins = list_all_protocol_auth_plugins()
items: list[dict[str, Any]] = []
for plugin in framework_plugins:
auth_type = plugin["type"]
items.append(
{
"type": auth_type,
"display_name": plugin["display_name"],
"is_framework": True,
"adapter_type": None,
"is_token_based": is_token_based_auth_type(auth_type, adapter_type=None),
"has_refresh_strategy": token_refresh_registry.get_strategy(auth_type) is not None,
}
)
for plugin in protocol_plugins:
auth_type = plugin["type"]
adapter_type = plugin["adapter_type"]
items.append(
{
"type": auth_type,
"display_name": plugin["display_name"],
"is_framework": False,
"adapter_type": adapter_type,
"is_token_based": is_token_based_auth_type(auth_type, adapter_type=adapter_type),
"has_refresh_strategy": token_refresh_registry.get_strategy(auth_type) is not None,
}
)
framework_count = len(framework_plugins)
protocol_count = len(protocol_plugins)
return {
"success": True,
"data": {
"items": items,
"total": framework_count + protocol_count,
"framework_count": framework_count,
"protocol_count": protocol_count,
},
}
@auth_plugin_router.get("/overview", response_model=dict)
async def get_auth_plugins_overview(
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询认证插件的分组总览(框架级 + 按适配器分组的协议专属)。
适合前端管理页树形渲染:``framework`` 列表为框架级插件,``protocol`` 字典
按 ``adapter_type`` 分组协议专属插件。每个条目标记 ``is_token_based`` 与
``has_refresh_strategy``。
与 ``GET /auth-plugins`` 的差异:本端点返回分组结构(适合树形展示),
清单端点返回扁平列表(适合表格展示)。
"""
framework_plugins = list_auth_plugins(None)
protocol_plugins = list_all_protocol_auth_plugins()
framework_items = [
{
"type": plugin["type"],
"display_name": plugin["display_name"],
"is_token_based": is_token_based_auth_type(plugin["type"], adapter_type=None),
"has_refresh_strategy": token_refresh_registry.get_strategy(plugin["type"]) is not None,
}
for plugin in framework_plugins
]
protocol_dict: dict[str, list[dict[str, Any]]] = {}
for plugin in protocol_plugins:
adapter_type = plugin["adapter_type"]
auth_type = plugin["type"]
item = {
"type": auth_type,
"display_name": plugin["display_name"],
"is_token_based": is_token_based_auth_type(auth_type, adapter_type=adapter_type),
"has_refresh_strategy": token_refresh_registry.get_strategy(auth_type) is not None,
}
protocol_dict.setdefault(adapter_type, []).append(item)
total = len(framework_items) + sum(len(items) for items in protocol_dict.values())
return {
"success": True,
"data": {
"framework": framework_items,
"protocol": protocol_dict,
"total": total,
},
}
@auth_plugin_router.get("/token-strategies", response_model=dict)
async def get_token_strategies(
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询已注册的 Token 刷新策略清单。
数据来源为 ``token_refresh_registry.list_strategies()``,仅返回已注册刷新策略
的 auth_type 列表。所有条目的 ``is_token_based`` 与 ``has_refresh_strategy``
均为 True刷新策略仅注册于 Token 类认证)。
设计价值前端可据此区分三类认证——静态凭证、Token 类无自动刷新、
Token 类有自动刷新。
"""
auth_types = token_refresh_registry.list_strategies()
framework_plugins = list_auth_plugins(None)
display_name_map = {plugin["type"]: plugin["display_name"] for plugin in framework_plugins}
items = [
{
"auth_type": auth_type,
"display_name": display_name_map.get(auth_type, auth_type),
"is_token_based": True,
"has_refresh_strategy": True,
}
for auth_type in auth_types
]
return {"success": True, "data": {"items": items, "total": len(items)}}
# =============================================================================
# === 路径参数端点(/{auth_type} ===
# =============================================================================
@auth_plugin_router.get("/{auth_type}", response_model=dict)
async def get_auth_plugin_detail(
auth_type: str,
adapter_type: str | None = Query(None, description="协议专属插件适配器类型"),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询指定认证插件的详情含展示信息、Token 标记、刷新策略、凭证字段)。
``adapter_type`` 为 None 时查框架级插件;不为 None 时优先查协议专属插件,
未命中回退框架级(协议复用框架插件)。``is_framework`` 据此判断:
框架级插件为 True协议专属插件为 False。
``AuthPluginNotFoundError`` 由全局处理器映射为 404Router 内不捕获。
"""
plugin = get_auth_plugin(auth_type, adapter_type=adapter_type)
if adapter_type is None:
is_framework = AUTH_PLUGINS.get(auth_type) is not None
response_adapter_type: str | None = None
elif get_protocol_auth_plugin(adapter_type, auth_type) is not None:
is_framework = False
response_adapter_type = adapter_type
else:
# 协议复用框架插件
is_framework = True
response_adapter_type = None
has_refresh_strategy = token_refresh_registry.get_strategy(auth_type) is not None
credential_type = plugin.credential_type
credential_fields = list(credential_type.model_fields.keys()) if credential_type is not None else []
data = {
"type": auth_type,
"display_name": plugin.display_name or auth_type,
"is_framework": is_framework,
"adapter_type": response_adapter_type,
"is_token_based": is_token_based_auth_type(auth_type, adapter_type=adapter_type),
"has_refresh_strategy": has_refresh_strategy,
"credential_type": credential_type.__name__ if credential_type is not None else None,
"credential_fields": credential_fields,
}
return {"success": True, "data": data}
@auth_plugin_router.get("/{auth_type}/config-schema", response_model=dict)
async def get_auth_plugin_config_schema(
auth_type: str,
adapter_type: str | None = Query(None, description="协议专属插件适配器类型"),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""获取认证配置的 JSON Schema用于前端动态表单渲染。
``adapter_type`` 不为 None 时优先查协议专属凭证类型,未命中回退框架级。
未知 ``auth_type``(插件不存在)抛 ``AuthPluginNotFoundError`` (404)
插件存在但无凭证类型(``credential_type=None``)抛 ``DomainValidationError``
(400)均由全局处理器映射Router 内不捕获。
"""
# 校验插件存在(抛 AuthPluginNotFoundError → 404Router 内不捕获)
get_auth_plugin(auth_type, adapter_type=adapter_type)
schema = get_auth_json_schema(auth_type, adapter_type=adapter_type)
return {
"success": True,
"data": {
"auth_type": auth_type,
"adapter_type": adapter_type,
"schema": schema,
},
}
@auth_plugin_router.post("/{auth_type}/validate", response_model=dict)
async def validate_auth_config(
auth_type: str,
body: ValidateAuthConfigRequest,
adapter_type: str | None = Query(None, description="协议专属插件适配器类型"),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""校验认证配置是否有效(解析凭证,不执行网络调用)。
调用 ``resolve_credential`` 进行完整凭证解析校验(含必填字段检查与密钥引用
解析),而非仅 Pydantic 结构校验。``AuthPluginNotFoundError`` (404) 由全局
处理器映射Router 内不捕获;``AuthError`` / ``SecretResolutionError`` 为
校验失败结果Router 内显式捕获并返回 200 + ``valid=False``。
不返回解析后的凭证对象(含敏感信息)。
"""
# 校验插件存在(抛 AuthPluginNotFoundError → 404Router 内不捕获)
get_auth_plugin(auth_type, adapter_type=adapter_type)
is_token_based = is_token_based_auth_type(auth_type, adapter_type=adapter_type)
has_refresh_strategy = token_refresh_registry.get_strategy(auth_type) is not None
try:
await resolve_credential(
auth_type,
body.auth_config,
resolver=None,
adapter_type=adapter_type,
)
except (AuthError, SecretResolutionError) as exc:
return {
"success": True,
"data": {
"valid": False,
"errors": [str(exc)],
"is_token_based": is_token_based,
"has_refresh_strategy": has_refresh_strategy,
},
}
return {
"success": True,
"data": {
"valid": True,
"errors": [],
"is_token_based": is_token_based,
"has_refresh_strategy": has_refresh_strategy,
},
}