ForcePilot/backend/package/yuxi/channel/extensions/flock/security.py
Kris 0bad70ec19 feat(flock): 新增Flock团队协作IM渠道插件
实现了完整的Flock渠道接入能力,包含消息收发、Webhook事件监听、账号配置、安全校验、媒体文件处理等功能,支持私聊和群组聊天,适配ForcePilot插件规范。
2026-05-21 10:47:01 +08:00

170 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import logging
import re
from .config import _apply_env_overrides, _dict_to_account
from .constants import FLOCK_GROUP_ID_PATTERN, FLOCK_USER_ID_PATTERN
logger = logging.getLogger(__name__)
_user_id_re = re.compile(FLOCK_USER_ID_PATTERN)
_group_id_re = re.compile(FLOCK_GROUP_ID_PATTERN)
def resolve_dm_policy(config: dict, account_id: str = "default") -> dict:
account_data = config.get("accounts", {}).get(account_id, {})
account = _dict_to_account(account_data)
account = _apply_env_overrides(account)
return {"mode": account.dm_policy, "allow_from": account.dm_allow_from}
def resolve_group_policy(config: dict, account_id: str = "default") -> dict:
account_data = config.get("accounts", {}).get(account_id, {})
account = _dict_to_account(account_data)
account = _apply_env_overrides(account)
return {"mode": account.group_policy, "group_allow_from": account.group_allow_from}
async def check_allowlist(
peer_id: str,
channel_type: str,
config: dict,
account_id: str = "default",
) -> dict:
account_data = config.get("accounts", {}).get(account_id, {})
account = _dict_to_account(account_data)
account = _apply_env_overrides(account)
if channel_type == "direct":
policy = account.dm_policy
allow_list = account.dm_allow_from
else:
policy = account.group_policy
allow_list = account.group_allow_from
if policy == "open":
return {"allowed": True, "reason": ""}
if policy == "disabled":
return {"allowed": False, "reason": f"{channel_type} messaging is disabled"}
if policy in ("allowlist", "pairing"):
if peer_id in allow_list:
return {"allowed": True, "reason": ""}
return {"allowed": False, "reason": f"Peer {peer_id} not in {channel_type} allow list"}
return {"allowed": False, "reason": f"Unknown policy: {policy}"}
def collect_warnings(
config: dict,
account_id: str | None = None,
account: dict | None = None,
) -> list[str]:
warnings: list[str] = []
if account is None:
if account_id:
account = config.get("accounts", {}).get(account_id, {})
if account is None:
return warnings
acct = _dict_to_account(account)
acct = _apply_env_overrides(acct)
if acct.dm_policy == "open":
warnings.append("DM 策略为 open任何人可私信 Bot")
if acct.group_policy == "open":
warnings.append("Group 策略为 open任何群组可 @Bot")
if not acct.outgoing_webhook_token and not acct.event_listener_token:
warnings.append("Outgoing Webhook token 和 Event Listener token 均未配置Webhook 验证将跳过")
if acct.event_listener_token and not acct.app_secret:
warnings.append("Event Listener token 已配置但 app_secret 未配置Event Token HMAC 签名验证将跳过")
if not acct.bot_token and not acct.incoming_webhook_url:
warnings.append("bot_token 和 incoming_webhook_url 均未配置,无法发送消息")
return warnings
def collect_audit_findings(
config,
account_id=None,
account=None,
source_config=None,
ordered_account_ids=None,
has_explicit_account_path=False,
) -> list[dict]:
findings: list[dict] = []
if account is None and account_id:
account = config.get("accounts", {}).get(account_id, {})
if account is None:
return findings
acct = _dict_to_account(account)
acct = _apply_env_overrides(acct)
if not acct.bot_token and not acct.incoming_webhook_url:
findings.append({
"severity": "error",
"category": "auth",
"message": "bot_token 和 incoming_webhook_url 均未配置,无法发送消息",
})
if acct.dm_policy == "open" and not acct.dm_allow_from:
findings.append({
"severity": "warning",
"category": "dm_policy",
"message": "DM 策略为 open 且未配置 dm_allow_from任何人可私信 Bot",
})
if acct.group_policy == "open" and not acct.require_mention:
findings.append({
"severity": "warning",
"category": "group_policy",
"message": "Group 策略为 open 且 require_mention=False任何群组消息都会被处理",
})
if acct.event_listener_token and not acct.app_secret:
findings.append({
"severity": "error",
"category": "security",
"message": "Event Listener token 已配置但 app_secret 未配置Event Token HMAC 签名验证将跳过",
})
if not acct.event_listener_token and not acct.outgoing_webhook_token:
findings.append({
"severity": "info",
"category": "security",
"message": "Event Listener token 和 Outgoing Webhook token 均未配置Webhook 来源无法验证",
})
return findings
def normalize_allow_entry(entry: str) -> str:
stripped = entry.strip()
if not stripped.startswith("u:") and not stripped.startswith("g:"):
stripped = f"u:{stripped}"
return stripped
def is_valid_flock_user_id(id_str: str) -> bool:
return bool(_user_id_re.match(id_str))
def is_valid_flock_group_id(id_str: str) -> bool:
return bool(_group_id_re.match(id_str))
def resolve_require_mention(config: dict, account_id: str = "default") -> bool:
account_data = config.get("accounts", {}).get(account_id, {})
account = _dict_to_account(account_data)
account = _apply_env_overrides(account)
return account.require_mention