1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
364 lines
14 KiB
Python
364 lines
14 KiB
Python
"""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, ValidationError
|
||
from yuxi.external_systems.exceptions import AuthError, DomainValidationError, 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)
|
||
protocol_plugins = list_all_protocol_auth_plugins()
|
||
display_name_map = {plugin["type"]: plugin["display_name"] for plugin in framework_plugins}
|
||
# 协议专属插件可能使用与框架级相同的 auth_type(如 tls),但展示名可能不同;
|
||
# 刷新策略若由协议专属插件注册,应优先显示其 display_name。
|
||
display_name_map.update(
|
||
{plugin["type"]: plugin["display_name"] for plugin in protocol_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`` 由全局处理器映射为 404,Router 内不捕获。
|
||
"""
|
||
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 → 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) 由全局
|
||
处理器映射,Router 内不捕获;``AuthError`` / ``SecretResolutionError`` /
|
||
``DomainValidationError`` / Pydantic ``ValidationError`` 为校验失败结果,
|
||
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, DomainValidationError) as exc:
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"valid": False,
|
||
"errors": [str(exc)],
|
||
"is_token_based": is_token_based,
|
||
"has_refresh_strategy": has_refresh_strategy,
|
||
},
|
||
}
|
||
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,
|
||
},
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"valid": True,
|
||
"errors": [],
|
||
"is_token_based": is_token_based,
|
||
"has_refresh_strategy": has_refresh_strategy,
|
||
},
|
||
}
|