ForcePilot/backend/server/routers/external_systems/auth_plugin_router.py
Kris 6dd16ad3f8 refactor(external-systems-routers): 统一分页参数格式并完善各路由文档与校验
本次提交对多个外部系统路由进行了多维度优化:
1.  统一分页参数:将所有路由的`page = offset//limit +1`、`page_size=limit`替换为标准的`limit`+`offset`分页格式
2.  完善接口文档:补充多个端点的功能说明、参数含义与返回字段解释
3.  增强参数校验:新增字段长度限制、正则校验、枚举类型约束与业务逻辑校验
4.  优化代码复用:提取重复逻辑为辅助函数,减少样板代码
5.  修复接口问题:修正工具健康检查端点路径参数类型,优化导出接口响应格式
6.  补充异常处理:为批量操作添加异常捕获与日志记录,避免流程中断
2026-07-11 06:57:24 +08:00

345 lines
13 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 (
get_auth_plugin,
get_auth_json_schema,
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"],
)
# =============================================================================
# === 辅助函数 ===
# =============================================================================
def _auth_markers(
auth_type: str,
adapter_type: str | None,
registered_strategies: set[str],
) -> dict[str, bool]:
"""构建 is_token_based / has_refresh_strategy 标记list/overview 共用)。
将两个端点重复的标记计算逻辑收敛到此处,避免散落在各循环内。使用
``registered_strategies`` 集合成员判断替代逐条 ``get_strategy()`` 查询,
对同名 auth_type 跨多适配器(如 tls 跨 http/grpc/database只查一次注册表。
Args:
auth_type: 认证类型。
adapter_type: 适配器类型(框架级为 None
registered_strategies: 预先获取的已注册刷新策略 auth_type 集合。
Returns:
含 ``is_token_based`` 与 ``has_refresh_strategy`` 两个键的字典。
"""
return {
"is_token_based": is_token_based_auth_type(auth_type, adapter_type=adapter_type),
"has_refresh_strategy": auth_type in registered_strategies,
}
# =============================================================================
# === 静态路径端点(必须在 /{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()
registered_strategies = set(token_refresh_registry.list_strategies())
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,
**_auth_markers(auth_type, None, registered_strategies),
}
)
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,
**_auth_markers(auth_type, adapter_type, registered_strategies),
}
)
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()
registered_strategies = set(token_refresh_registry.list_strategies())
framework_items = [
{
"type": plugin["type"],
"display_name": plugin["display_name"],
**_auth_markers(plugin["type"], None, registered_strategies),
}
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"],
**_auth_markers(auth_type, adapter_type, registered_strategies),
}
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 类有自动刷新。
"""
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": is_token_based_auth_type(auth_type, adapter_type=None),
"has_refresh_strategy": token_refresh_registry.get_strategy(auth_type) is not None,
}
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:
# adapter_type 为 None 时 get_auth_plugin 仅查框架级表,命中即框架级
is_framework = True
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``。
不返回解析后的凭证对象(含敏感信息)。
``resolve_credential`` 内部会调用 ``get_auth_plugin`` 校验插件存在性,
未注册时抛 ``AuthPluginNotFoundError`` (404),无需 Router 重复前置校验。
"""
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,
},
}