1. 清理测试文件中未使用的导入与冗余代码 2. 修复审计日志与批量操作的空值约束,统一填充"global"作为默认渠道 3. 调整批量消息撤回的响应语义,对齐其他端点的部分成功契约 4. 修复访问规则批量克隆的唯一约束问题,新增后缀自动处理逻辑 5. 替换anyio为asyncio并行调用,修正时间UTC导入路径 6. 优化前端外部系统概览页的刷新状态提示与缓存逻辑 7. 修复测试用例中的断言与请求方式问题,适配httpx删除请求特性 8. 重构后端路由的依赖注入,移除冗余的数据库会话依赖 9. 调整测试用例的权限校验逻辑,修正强制登出的权限判断 10. 修复语义分块测试的numpy依赖问题,清理冗余导入
369 lines
15 KiB
Python
369 lines
15 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.auth_plugins.credentials import NoneCredential
|
||
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
|
||
# NoneCredential 为无认证凭证标记类,序列化时归一化为 None(对齐 none 插件
|
||
# "无凭证类型" 语义,避免输出 "NoneCredential" 字符串误导前端)
|
||
if credential_type is NoneCredential:
|
||
credential_type = None
|
||
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,
|
||
},
|
||
}
|