ForcePilot/backend/package/yuxi/channel/extensions/flock/security.py

170 lines
5.5 KiB
Python
Raw Normal View History

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