163 lines
6.4 KiB
Python
163 lines
6.4 KiB
Python
"""ZYT 管理员身份服务适配器。
|
||
|
||
桌面软件的密码只发送给 ZYT;客服后台拿到管理员 token 后通过最小会话接口验证,
|
||
再签发仅能访问客服数据的短期 token。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import socket
|
||
import urllib.error
|
||
import urllib.parse
|
||
import urllib.request
|
||
from typing import Any
|
||
|
||
|
||
WECOM_RPA_TERMINAL = 7
|
||
DEFAULT_ZYT_API_BASE_URL = os.environ.get(
|
||
"ZYT_API_BASE_URL", "http://127.0.0.1"
|
||
).strip().rstrip("/")
|
||
|
||
|
||
class ZytAuthError(RuntimeError):
|
||
pass
|
||
|
||
|
||
def api_base_url() -> str:
|
||
value = DEFAULT_ZYT_API_BASE_URL
|
||
parsed = urllib.parse.urlparse(value)
|
||
if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username:
|
||
raise ZytAuthError("ZYT_API_BASE_URL 配置不正确")
|
||
return value.rstrip("/")
|
||
|
||
|
||
def verify_desktop_token(
|
||
token: str, *, api_url: str | None = None, timeout: float = 8.0
|
||
) -> dict[str, Any]:
|
||
token = str(token or "").strip()
|
||
if not token:
|
||
raise ZytAuthError("缺少 ZYT 登录令牌")
|
||
base_url = str(api_url or api_base_url()).strip().rstrip("/")
|
||
parsed = urllib.parse.urlparse(base_url)
|
||
if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username:
|
||
raise ZytAuthError("ZYT 登录 API 域名配置不正确")
|
||
last_error = "ZYT 登录已失效"
|
||
# 第一条是新版专用接口;第二条兼容尚未发布桌面终端的线上旧版。
|
||
endpoints = (
|
||
("/adminapi/desktop/session", False),
|
||
("/adminapi/auth.admin/mySelf", True),
|
||
)
|
||
for path, legacy in endpoints:
|
||
request = urllib.request.Request(
|
||
f"{base_url}{path}",
|
||
headers={
|
||
"Accept": "application/json",
|
||
"User-Agent": "WeCom-RPA-Backend/1.0",
|
||
# ZYT 管理端登录中间件读取 token 头,不是 Bearer。
|
||
"token": token,
|
||
},
|
||
method="GET",
|
||
)
|
||
try:
|
||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||
payload = json.loads(response.read().decode("utf-8"))
|
||
except urllib.error.HTTPError as exc:
|
||
last_error = f"ZYT 身份服务拒绝请求(HTTP {exc.code})"
|
||
# 只兼容尚未部署的路由;鉴权拒绝不能降级为网页身份。
|
||
if not legacy and exc.code in {404, 405}:
|
||
continue
|
||
raise ZytAuthError(last_error) from exc
|
||
except (urllib.error.URLError, TimeoutError, socket.timeout) as exc:
|
||
raise ZytAuthError(
|
||
f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}"
|
||
) from exc
|
||
except (ValueError, TypeError, UnicodeDecodeError) as exc:
|
||
raise ZytAuthError("ZYT 身份服务返回了无效数据") from exc
|
||
|
||
if not isinstance(payload, dict):
|
||
raise ZytAuthError("ZYT 身份服务返回了无效数据")
|
||
if str(payload.get("code")) != "1":
|
||
last_error = str(
|
||
payload.get("msg") if isinstance(payload, dict) else ""
|
||
) or "ZYT 登录已失效"
|
||
if legacy and "ip" in last_error.lower() and "变化" in last_error:
|
||
raise ZytAuthError(
|
||
"ZYT 桌面认证接口尚未部署,旧版接口拒绝跨服务器验证。"
|
||
"请管理员部署 /adminapi/desktop/session 后重新登录"
|
||
)
|
||
raise ZytAuthError(last_error)
|
||
data = payload.get("data")
|
||
if not isinstance(data, dict):
|
||
raise ZytAuthError("ZYT 身份信息不完整")
|
||
|
||
if legacy:
|
||
admin = data.get("user")
|
||
if not isinstance(admin, dict) or not admin.get("id"):
|
||
raise ZytAuthError("ZYT 管理员身份信息不完整")
|
||
if int(admin.get("disable") or 0) != 0:
|
||
raise ZytAuthError("ZYT 管理员账号已停用")
|
||
permissions_value = data.get("permissions")
|
||
permissions = (
|
||
sorted(
|
||
{
|
||
str(item).strip()
|
||
for item in permissions_value
|
||
if str(item).strip()
|
||
}
|
||
)
|
||
if isinstance(permissions_value, list)
|
||
else []
|
||
)
|
||
role_ids_value = admin.get("role_ids")
|
||
role_ids: list[int] = []
|
||
if isinstance(role_ids_value, list):
|
||
for item in role_ids_value:
|
||
try:
|
||
role_ids.append(int(item))
|
||
except (TypeError, ValueError):
|
||
continue
|
||
profile_identity = {
|
||
"user_id": f"admin:{admin['id']}",
|
||
"sn": str(admin.get("account") or ""),
|
||
"nickname": str(admin.get("name") or admin.get("account") or ""),
|
||
"mobile": str(admin.get("phone") or ""),
|
||
"avatar": str(admin.get("avatar") or ""),
|
||
"terminal": WECOM_RPA_TERMINAL,
|
||
"status": "active",
|
||
"account_type": "admin",
|
||
"root": bool(int(admin.get("root") or 0)),
|
||
"role_ids": sorted(set(role_ids)),
|
||
"permissions": permissions,
|
||
"permissions_known": isinstance(permissions_value, list),
|
||
}
|
||
return profile_identity
|
||
|
||
identity = dict(data)
|
||
if not identity.get("user_id"):
|
||
raise ZytAuthError("ZYT 身份信息不完整")
|
||
if str(identity.get("terminal")) != str(WECOM_RPA_TERMINAL):
|
||
raise ZytAuthError("当前令牌不是企业微信客服桌面端令牌")
|
||
if str(identity.get("status") or "") != "active":
|
||
raise ZytAuthError("ZYT 管理员账号已停用")
|
||
permissions_value = identity.get("permissions")
|
||
identity["permissions"] = (
|
||
sorted(
|
||
{
|
||
str(item).strip()
|
||
for item in permissions_value
|
||
if str(item).strip()
|
||
}
|
||
)
|
||
if isinstance(permissions_value, list)
|
||
else []
|
||
)
|
||
identity["permissions_known"] = isinstance(permissions_value, list)
|
||
identity["root"] = bool(int(identity.get("root") or 0))
|
||
# 新版专用接口完整返回权限;旧版缺权限时保留未知状态,患者功能
|
||
# 按最小权限关闭,不再跨 IP 请求网页 mySelf 补权限。
|
||
return identity
|
||
|
||
raise ZytAuthError(last_error)
|