"""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`` 由全局处理器映射为 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`` 为 校验失败结果,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, }, }