ForcePilot/backend/package/yuxi/channels/adapters/zalo_oa/security.py
Kris 30dd9e16f4 feat(zalo-oa): 实现完整的 Zalo OA 渠道适配器模块
新增 Zalo OA 官方账号完整集成能力,包含:
1. 基础通信能力:消息编解码、目标归一化、文本分块
2. 安全与校验:Webhook 签名验证、DM 策略管理、配对流程
3. 辅助工具:重复事件去重、请求限流、异常告警
4. 管理功能:账号多实例管理、配置验证、健康诊断
5. 扩展能力:媒体托管、视觉识别、TTS 语音合成
6. 运维支持:审计日志、状态监控、目录同步
2026-05-12 00:52:47 +08:00

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