"""外部系统认证策略。 认证只消费管理员保存的可信配置和用户加密保存的字段,不允许模型指定认证地址或请求头。 """ from __future__ import annotations import json from dataclasses import dataclass from typing import Any, Protocol from urllib.parse import urljoin import httpx class ExternalAuthError(RuntimeError): pass @dataclass(frozen=True) class CredentialField: name: str label: str secret: bool = True autocomplete: str = "off" def as_dict(self) -> dict[str, Any]: return { "name": self.name, "label": self.label, "secret": self.secret, "autocomplete": self.autocomplete, } class AuthStrategy(Protocol): credential_fields: tuple[CredentialField, ...] def headers( self, *, client: httpx.Client, base_url: str, credentials: dict[str, str], config: dict[str, Any], ) -> dict[str, str]: ... def _required(credentials: dict[str, str], fields: tuple[CredentialField, ...]) -> None: missing = [ field.label for field in fields if not credentials.get(field.name, "").strip() ] if missing: raise ExternalAuthError("请填写" + "、".join(missing)) def _nested_value(payload: Any, path: str) -> Any: current = payload for part in path.split("."): if not isinstance(current, dict): return None current = current.get(part) return current def _auth_header( config: dict[str, Any], token: str, *, default_name: str, default_template: str ) -> dict[str, str]: name = str(config.get("auth_header_name") or default_name).strip() template = str(config.get("auth_header_template") or default_template) if any(char in name for char in "\r\n:") or any( char in template for char in "\r\n" ): raise ExternalAuthError("认证 Header 配置非法") if "{token}" not in template: raise ExternalAuthError("认证 Header 模板必须包含 {token}") return {name: template.replace("{token}", token)} class PasswordJwtAuth: credential_fields: tuple[CredentialField, ...] = ( CredentialField("username", "用户名", secret=False, autocomplete="username"), CredentialField("password", "密码", autocomplete="current-password"), ) def headers( self, *, client: httpx.Client, base_url: str, credentials: dict[str, str], config: dict[str, Any], ) -> dict[str, str]: _required(credentials, self.credential_fields) login_path = str(config.get("login_path") or "/api/auth/token/").strip() if not login_path.startswith("/") or "://" in login_path: raise ExternalAuthError("login_path 必须是站内绝对路径") username_field = str(config.get("username_field") or "username").strip() password_field = str(config.get("password_field") or "password").strip() token_field = str(config.get("token_field") or "access").strip() try: with client.stream( "POST", urljoin(base_url + "/", login_path.lstrip("/")), json={ username_field: credentials["username"], password_field: credentials["password"], }, ) as response: chunks: list[bytes] = [] total = 0 for chunk in response.iter_bytes(): total += len(chunk) if total > 65536: raise ExternalAuthError("外部系统登录响应超过安全上限") chunks.append(chunk) status_code = response.status_code content = b"".join(chunks) except httpx.HTTPError as exc: raise ExternalAuthError( f"外部系统登录连接失败: {type(exc).__name__}" ) from exc if status_code >= 400: raise ExternalAuthError(f"外部系统登录失败(HTTP {status_code})") try: token = _nested_value(json.loads(content.decode("utf-8-sig")), token_field) except (UnicodeDecodeError, ValueError): token = None if not isinstance(token, str) or not token: raise ExternalAuthError(f"外部系统登录响应缺少 {token_field}") return _auth_header( config, token, default_name="Authorization", default_template="Bearer {token}", ) class ApiKeyAuth: credential_fields: tuple[CredentialField, ...] = ( CredentialField("api_key", "API Key"), ) def headers( self, *, client: httpx.Client, base_url: str, credentials: dict[str, str], config: dict[str, Any], ) -> dict[str, str]: _required(credentials, self.credential_fields) return _auth_header( config, credentials["api_key"], default_name="X-API-Key", default_template="{token}", ) class BearerTokenAuth: credential_fields: tuple[CredentialField, ...] = ( CredentialField("token", "Bearer Token"), ) def headers( self, *, client: httpx.Client, base_url: str, credentials: dict[str, str], config: dict[str, Any], ) -> dict[str, str]: _required(credentials, self.credential_fields) return _auth_header( config, credentials["token"], default_name="Authorization", default_template="Bearer {token}", ) _AUTH_STRATEGIES: dict[str, AuthStrategy] = { "password_jwt": PasswordJwtAuth(), "api_key": ApiKeyAuth(), "bearer_token": BearerTokenAuth(), } def get_auth_strategy(auth_type: str) -> AuthStrategy: strategy = _AUTH_STRATEGIES.get((auth_type or "").strip()) if strategy is None: raise ExternalAuthError(f"不支持的认证方式: {auth_type}") return strategy def auth_catalog() -> list[dict[str, Any]]: titles = { "password_jwt": "用户名密码换取 Token", "api_key": "API Key", "bearer_token": "Bearer Token", } return [ { "auth_type": key, "title": titles[key], "credential_fields": [ field.as_dict() for field in strategy.credential_fields ], } for key, strategy in _AUTH_STRATEGIES.items() ]