新增 Zalo OA 官方账号完整集成能力,包含: 1. 基础通信能力:消息编解码、目标归一化、文本分块 2. 安全与校验:Webhook 签名验证、DM 策略管理、配对流程 3. 辅助工具:重复事件去重、请求限流、异常告警 4. 管理功能:账号多实例管理、配置验证、健康诊断 5. 扩展能力:媒体托管、视觉识别、TTS 语音合成 6. 运维支持:审计日志、状态监控、目录同步
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import enum
|
|
from typing import Any
|
|
|
|
ALLOW_ENTRY_PREFIXES = ("zoa:", "zalo_oa:")
|
|
|
|
|
|
class DMPolicy(enum.StrEnum):
|
|
OPEN = "open"
|
|
PAIRING = "pairing"
|
|
ALLOWLIST = "allowlist"
|
|
DISABLED = "disabled"
|
|
|
|
|
|
def normalize_allow_entry(entry: str) -> str:
|
|
for prefix in ALLOW_ENTRY_PREFIXES:
|
|
if entry.lower().startswith(prefix):
|
|
return entry[len(prefix) :]
|
|
return entry
|
|
|
|
|
|
def resolve_dm_policy(config: dict[str, Any]) -> DMPolicy:
|
|
policy_str = config.get("dm_policy", config.get("dmPolicy", ""))
|
|
if policy_str == "open":
|
|
return DMPolicy.OPEN
|
|
if policy_str == "pairing":
|
|
return DMPolicy.PAIRING
|
|
if policy_str == "allowlist":
|
|
return DMPolicy.ALLOWLIST
|
|
if policy_str == "disabled":
|
|
return DMPolicy.DISABLED
|
|
return DMPolicy.OPEN
|
|
|
|
|
|
def load_allowlist(config: dict[str, Any]) -> set[str]:
|
|
raw = config.get("allowFrom", [])
|
|
if not isinstance(raw, list):
|
|
raw = []
|
|
if "*" in raw:
|
|
return {"*"}
|
|
return {normalize_allow_entry(str(entry)) for entry in raw}
|
|
|
|
|
|
def check_dm_allowed(user_id: str, policy: DMPolicy, allowlist: set[str]) -> bool:
|
|
if policy == DMPolicy.OPEN:
|
|
return True
|
|
if policy == DMPolicy.DISABLED:
|
|
return False
|
|
if policy == DMPolicy.ALLOWLIST:
|
|
if "*" in allowlist:
|
|
return True
|
|
return user_id in allowlist
|
|
if policy == DMPolicy.PAIRING:
|
|
if "*" in allowlist:
|
|
return True
|
|
return user_id in allowlist
|
|
return False
|
|
|
|
|
|
def collect_security_warnings(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
warnings: list[dict[str, Any]] = []
|
|
policy = resolve_dm_policy(config)
|
|
allowlist = load_allowlist(config)
|
|
webhook = config.get("webhook", {})
|
|
|
|
if policy == DMPolicy.OPEN:
|
|
warnings.append(
|
|
{
|
|
"severity": "warning",
|
|
"type": "dm_policy_open",
|
|
"message": "DM policy is 'open' — any follower can send messages without restriction",
|
|
"recommendation": "Consider switching to 'allowlist' or 'pairing' for production",
|
|
}
|
|
)
|
|
|
|
if policy == DMPolicy.ALLOWLIST and not allowlist:
|
|
warnings.append(
|
|
{
|
|
"severity": "error",
|
|
"type": "empty_allowlist",
|
|
"message": "DM policy is 'allowlist' but allowFrom is empty — no users can message the OA",
|
|
"recommendation": "Add user IDs to allowFrom or change policy to 'open'",
|
|
}
|
|
)
|
|
|
|
if policy == DMPolicy.PAIRING and not allowlist and "*" not in allowlist:
|
|
warnings.append(
|
|
{
|
|
"severity": "info",
|
|
"type": "pairing_no_preset",
|
|
"message": "DM policy is 'pairing' with empty allowFrom — all pairing requests will be processed",
|
|
"recommendation": "Add pre-approved user IDs to allowFrom for faster onboarding",
|
|
}
|
|
)
|
|
|
|
if policy == DMPolicy.ALLOWLIST and "*" in allowlist:
|
|
warnings.append(
|
|
{
|
|
"severity": "info",
|
|
"type": "allowlist_wildcard",
|
|
"message": "allowFrom contains '*' wildcard — effectively same as 'open' policy",
|
|
"recommendation": "Use '*' only in development or use 'open' policy explicitly",
|
|
}
|
|
)
|
|
|
|
if not webhook.get("url"):
|
|
warnings.append(
|
|
{
|
|
"severity": "info",
|
|
"type": "webhook_not_configured",
|
|
"message": "Webhook URL not configured — messages will only be received via polling",
|
|
"recommendation": "Configure a webhook URL for real-time message delivery",
|
|
}
|
|
)
|
|
|
|
if not webhook.get("mac_key"):
|
|
warnings.append(
|
|
{
|
|
"severity": "warning",
|
|
"type": "webhook_mac_key_missing",
|
|
"message": "Webhook MAC key not configured — webhook signature verification is disabled",
|
|
"recommendation": "Set ZALO_WEBHOOK_MAC_KEY or webhook.mac_key to enable signature verification",
|
|
}
|
|
)
|
|
|
|
return warnings
|