aps-agent/server/integrations/jms_auth_client.py

228 lines
8.9 KiB
Python
Raw Normal View History

2026-07-29 23:22:40 +08:00
from __future__ import annotations
import os
from typing import Any
from urllib.parse import urlparse
import httpx
class JmsAuthClientError(Exception):
def __init__(self, code: str, message: str, status_code: int) -> None:
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
class JmsAuthClient:
"""Small typed boundary around the JMS authentication endpoints."""
def __init__(
self,
*,
base_url: str | None = None,
timeout_seconds: float | None = None,
transport: httpx.AsyncBaseTransport | None = None,
) -> None:
configured_base_url = (base_url or os.environ.get("JMS_AUTH_BASE_URL") or "").strip()
if not configured_base_url:
raise JmsAuthClientError(
"AUTH_CONFIG_INVALID", "JMS 认证服务地址未配置(JMS_AUTH_BASE_URL)", 503,
)
self.base_url = configured_base_url.rstrip("/")
2026-07-29 23:22:40 +08:00
try:
self.timeout_seconds = timeout_seconds or float(
os.environ.get("JMS_AUTH_TIMEOUT_SECONDS") or "10"
)
except ValueError as exc:
raise JmsAuthClientError(
"AUTH_CONFIG_INVALID", "JMS 认证超时时间配置无效", 503,
) from exc
if self.timeout_seconds <= 0:
raise JmsAuthClientError(
"AUTH_CONFIG_INVALID", "JMS 认证超时时间必须大于 0", 503,
)
self.transport = transport
parsed = urlparse(self.base_url)
if parsed.scheme != "https" or not parsed.netloc:
raise JmsAuthClientError(
"AUTH_CONFIG_INVALID",
"JMS 认证服务地址必须是有效的 HTTPS 地址",
503,
)
async def _request(
self,
method: str,
path: str,
*,
params: dict[str, str] | None = None,
json_body: dict[str, Any] | None = None,
form_body: dict[str, str] | None = None,
token_name: str | None = None,
token_value: str | None = None,
) -> Any:
headers: dict[str, str] = {"Accept": "application/json"}
if token_name and token_value:
headers[token_name] = token_value
try:
async with httpx.AsyncClient(
base_url=self.base_url,
timeout=self.timeout_seconds,
follow_redirects=False,
transport=self.transport,
) as client:
response = await client.request(
method,
path,
params=params,
json=json_body,
data=form_body,
headers=headers,
)
except httpx.TimeoutException as exc:
raise JmsAuthClientError(
"AUTH_UPSTREAM_TIMEOUT", "JMS 认证服务响应超时", 504,
) from exc
except httpx.RequestError as exc:
raise JmsAuthClientError(
"AUTH_UPSTREAM_UNAVAILABLE", "无法连接 JMS 认证服务", 502,
) from exc
try:
envelope = response.json()
except ValueError as exc:
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 认证服务返回了无效响应", 502,
) from exc
if not isinstance(envelope, dict):
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 认证服务返回了无效响应", 502,
)
remote_code = envelope.get("code", 0)
success = response.is_success and remote_code in (0, "0", None)
if not success:
message = str(envelope.get("message") or envelope.get("msg") or "JMS 认证请求失败")
numeric_code = int(remote_code) if str(remote_code).isdigit() else response.status_code
if path == "/admin/index/login":
diagnostic = f"{message} {envelope.get('error') or ''}".lower()
if "验证码" in diagnostic or "captcha" in diagnostic:
raise JmsAuthClientError(
"AUTH_CAPTCHA_INVALID", "验证码错误,请重新输入", 400,
)
if any(word in diagnostic for word in ("停用", "禁用", "锁定", "disabled", "locked")):
raise JmsAuthClientError(
"AUTH_USER_DISABLED", "账号已停用或锁定,请联系管理员", 403,
)
if any(word in diagnostic for word in ("租户", "企业", "tenant")):
raise JmsAuthClientError(
"AUTH_TENANT_INVALID", "企业名称或账号不匹配", 400,
)
credential_rejected = (
response.status_code in {400, 401, 403}
or numeric_code in {400, 401, 403}
or any(word in diagnostic for word in (
"validation failure", "bad_request", "bad request",
"用户名", "用户不存在", "账号", "密码", "credential",
))
)
if credential_rejected:
raise JmsAuthClientError(
"AUTH_INVALID_CREDENTIALS", "账号或密码错误", 401,
)
raise JmsAuthClientError(
"AUTH_UPSTREAM_ERROR", "认证服务暂时不可用,请稍后重试", 502,
)
if response.status_code >= 500 or numeric_code >= 500:
raise JmsAuthClientError("AUTH_UPSTREAM_ERROR", message, 502)
status_code = response.status_code if response.status_code in {400, 401, 403} else 400
raise JmsAuthClientError("AUTH_UPSTREAM_REJECTED", message, status_code)
return envelope.get("data")
async def precheck(self, *, tenant_code: str, username: str = "") -> dict[str, Any]:
data = await self._request(
"GET",
"/admin/index/login/precheck",
params={"tenantCode": tenant_code, "username": username},
)
if not isinstance(data, dict):
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 登录预检响应格式无效", 502,
)
return data
async def tenants(self) -> list[dict[str, Any]]:
data = await self._request("GET", "/admin/index/tenant")
if not isinstance(data, list):
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 租户列表响应格式无效", 502,
)
return [row for row in data if isinstance(row, dict)]
async def captcha(self) -> dict[str, str]:
data = await self._request("GET", "/admin/index/captcha")
if not isinstance(data, dict):
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 验证码响应格式无效", 502,
)
return {str(key): str(value) for key, value in data.items() if value is not None}
async def login(
self,
*,
tenant_code: str,
tenant_name: str,
username: str,
password: str,
captcha_id: str = "",
captcha_code: str = "",
) -> dict[str, Any]:
form_body = {
"tenantCode": tenant_code,
"tenantName": tenant_name,
"username": username,
"password": password,
"device": "PC",
}
if captcha_id:
form_body["captchaId"] = captcha_id
if captcha_code:
form_body["captchaCode"] = captcha_code
data = await self._request(
"POST",
"/admin/index/login",
form_body=form_body,
)
if not isinstance(data, dict):
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 登录响应格式无效", 502,
)
return data
async def profile(self, *, token_name: str, token_value: str) -> dict[str, Any]:
data = await self._request(
"GET", "/admin/auth/profile", token_name=token_name, token_value=token_value,
)
if not isinstance(data, dict):
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 用户信息响应格式无效", 502,
)
return data
async def tenant(self, *, token_name: str, token_value: str) -> dict[str, Any]:
data = await self._request(
"GET", "/admin/auth/tenant", token_name=token_name, token_value=token_value,
)
if not isinstance(data, dict):
raise JmsAuthClientError(
"AUTH_UPSTREAM_INVALID", "JMS 租户信息响应格式无效", 502,
)
return data
async def logout(self, *, token_name: str, token_value: str) -> None:
await self._request(
"POST", "/admin/auth/logout", token_name=token_name, token_value=token_value,
)