2026-06-20 22:16:07 +08:00
|
|
|
|
"""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
|
2026-07-11 21:39:05 +08:00
|
|
|
|
from pydantic import BaseModel, ValidationError
|
|
|
|
|
|
from yuxi.external_systems.exceptions import AuthError, DomainValidationError, SecretResolutionError
|
2026-06-20 22:16:07 +08:00
|
|
|
|
from yuxi.external_systems.framework.auth_plugins import (
|
|
|
|
|
|
get_auth_plugin,
|
2026-07-11 06:57:24 +08:00
|
|
|
|
get_auth_json_schema,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 06:57:24 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 辅助函数 ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 22:16:07 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 静态路径端点(必须在 /{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()
|
2026-07-11 06:57:24 +08:00
|
|
|
|
registered_strategies = set(token_refresh_registry.list_strategies())
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-07-11 06:57:24 +08:00
|
|
|
|
**_auth_markers(auth_type, None, registered_strategies),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
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,
|
2026-07-11 06:57:24 +08:00
|
|
|
|
**_auth_markers(auth_type, adapter_type, registered_strategies),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2026-07-11 06:57:24 +08:00
|
|
|
|
registered_strategies = set(token_refresh_registry.list_strategies())
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
framework_items = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": plugin["type"],
|
|
|
|
|
|
"display_name": plugin["display_name"],
|
2026-07-11 06:57:24 +08:00
|
|
|
|
**_auth_markers(plugin["type"], None, registered_strategies),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
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"],
|
2026-07-11 06:57:24 +08:00
|
|
|
|
**_auth_markers(auth_type, adapter_type, registered_strategies),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
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()``,仅返回已注册刷新策略
|
2026-07-11 06:57:24 +08:00
|
|
|
|
的 auth_type 列表。``is_token_based`` 与 ``has_refresh_strategy`` 通过真实查询
|
|
|
|
|
|
获取(而非硬编码 True),确保框架约束变化时端点数据保持一致。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
设计价值:前端可据此区分三类认证——静态凭证、Token 类无自动刷新、
|
|
|
|
|
|
Token 类有自动刷新。
|
|
|
|
|
|
"""
|
|
|
|
|
|
auth_types = token_refresh_registry.list_strategies()
|
|
|
|
|
|
framework_plugins = list_auth_plugins(None)
|
2026-07-11 21:39:05 +08:00
|
|
|
|
protocol_plugins = list_all_protocol_auth_plugins()
|
2026-06-20 22:16:07 +08:00
|
|
|
|
display_name_map = {plugin["type"]: plugin["display_name"] for plugin in framework_plugins}
|
2026-07-11 21:39:05 +08:00
|
|
|
|
# 协议专属插件可能使用与框架级相同的 auth_type(如 tls),但展示名可能不同;
|
|
|
|
|
|
# 刷新策略若由协议专属插件注册,应优先显示其 display_name。
|
|
|
|
|
|
display_name_map.update(
|
|
|
|
|
|
{plugin["type"]: plugin["display_name"] for plugin in protocol_plugins}
|
|
|
|
|
|
)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
items = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"auth_type": auth_type,
|
|
|
|
|
|
"display_name": display_name_map.get(auth_type, auth_type),
|
2026-07-11 06:57:24 +08:00
|
|
|
|
"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,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
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`` 由全局处理器映射为 404,Router 内不捕获。
|
|
|
|
|
|
"""
|
|
|
|
|
|
plugin = get_auth_plugin(auth_type, adapter_type=adapter_type)
|
|
|
|
|
|
|
|
|
|
|
|
if adapter_type is None:
|
2026-07-11 06:57:24 +08:00
|
|
|
|
# adapter_type 为 None 时 get_auth_plugin 仅查框架级表,命中即框架级
|
|
|
|
|
|
is_framework = True
|
2026-06-20 22:16:07 +08:00
|
|
|
|
response_adapter_type: str | None = None
|
|
|
|
|
|
elif get_protocol_auth_plugin(adapter_type, auth_type) is not None:
|
2026-07-11 06:57:24 +08:00
|
|
|
|
# 协议专属表命中
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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 → 404,Router 内不捕获)
|
|
|
|
|
|
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) 由全局
|
2026-07-11 21:39:05 +08:00
|
|
|
|
处理器映射,Router 内不捕获;``AuthError`` / ``SecretResolutionError`` /
|
|
|
|
|
|
``DomainValidationError`` / Pydantic ``ValidationError`` 为校验失败结果,
|
|
|
|
|
|
Router 内显式捕获并返回 200 + ``valid=False``。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
不返回解析后的凭证对象(含敏感信息)。
|
|
|
|
|
|
|
2026-07-11 06:57:24 +08:00
|
|
|
|
``resolve_credential`` 内部会调用 ``get_auth_plugin`` 校验插件存在性,
|
|
|
|
|
|
未注册时抛 ``AuthPluginNotFoundError`` (404),无需 Router 重复前置校验。
|
|
|
|
|
|
"""
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-07-11 21:39:05 +08:00
|
|
|
|
except (AuthError, SecretResolutionError, DomainValidationError) as exc:
|
2026-06-20 22:16:07 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"valid": False,
|
|
|
|
|
|
"errors": [str(exc)],
|
|
|
|
|
|
"is_token_based": is_token_based,
|
|
|
|
|
|
"has_refresh_strategy": has_refresh_strategy,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
2026-07-11 21:39:05 +08:00
|
|
|
|
except ValidationError as exc:
|
|
|
|
|
|
# Pydantic 校验失败(如字段类型不匹配),errors 格式化为 list[str],
|
|
|
|
|
|
# 与 adapter_router / system_router 校验端点对齐
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"valid": False,
|
|
|
|
|
|
"errors": [f"{'.'.join(str(p) for p in e['loc'])}: {e['msg']}" for e in exc.errors()],
|
|
|
|
|
|
"is_token_based": is_token_based,
|
|
|
|
|
|
"has_refresh_strategy": has_refresh_strategy,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"valid": True,
|
|
|
|
|
|
"errors": [],
|
|
|
|
|
|
"is_token_based": is_token_based,
|
|
|
|
|
|
"has_refresh_strategy": has_refresh_strategy,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|