fix(telegram): support group user allowlist
This commit is contained in:
@@ -714,11 +714,21 @@ def load_gateway_config() -> GatewayConfig:
|
||||
os.environ["TELEGRAM_REACTIONS"] = str(telegram_cfg["reactions"]).lower()
|
||||
if "proxy_url" in telegram_cfg and not os.getenv("TELEGRAM_PROXY"):
|
||||
os.environ["TELEGRAM_PROXY"] = str(telegram_cfg["proxy_url"]).strip()
|
||||
if "group_allowed_chats" in telegram_cfg and not os.getenv("TELEGRAM_GROUP_ALLOWED_USERS"):
|
||||
gac = telegram_cfg["group_allowed_chats"]
|
||||
if isinstance(gac, list):
|
||||
gac = ",".join(str(v) for v in gac)
|
||||
os.environ["TELEGRAM_GROUP_ALLOWED_USERS"] = str(gac)
|
||||
allowed_users = telegram_cfg.get("allow_from")
|
||||
if allowed_users is not None and not os.getenv("TELEGRAM_ALLOWED_USERS"):
|
||||
if isinstance(allowed_users, list):
|
||||
allowed_users = ",".join(str(v) for v in allowed_users)
|
||||
os.environ["TELEGRAM_ALLOWED_USERS"] = str(allowed_users)
|
||||
group_allowed_users = telegram_cfg.get("group_allow_from")
|
||||
if group_allowed_users is not None and not os.getenv("TELEGRAM_GROUP_ALLOWED_USERS"):
|
||||
if isinstance(group_allowed_users, list):
|
||||
group_allowed_users = ",".join(str(v) for v in group_allowed_users)
|
||||
os.environ["TELEGRAM_GROUP_ALLOWED_USERS"] = str(group_allowed_users)
|
||||
group_allowed_chats = telegram_cfg.get("group_allowed_chats")
|
||||
if group_allowed_chats is not None and not os.getenv("TELEGRAM_GROUP_ALLOWED_CHATS"):
|
||||
if isinstance(group_allowed_chats, list):
|
||||
group_allowed_chats = ",".join(str(v) for v in group_allowed_chats)
|
||||
os.environ["TELEGRAM_GROUP_ALLOWED_CHATS"] = str(group_allowed_chats)
|
||||
if "disable_link_previews" in telegram_cfg:
|
||||
plat_data = platforms_data.setdefault(Platform.TELEGRAM.value, {})
|
||||
if not isinstance(plat_data, dict):
|
||||
|
||||
@@ -2345,6 +2345,8 @@ class GatewayRunner:
|
||||
for v in ("TELEGRAM_ALLOWED_USERS", "DISCORD_ALLOWED_USERS",
|
||||
"WHATSAPP_ALLOWED_USERS", "SLACK_ALLOWED_USERS",
|
||||
"SIGNAL_ALLOWED_USERS", "SIGNAL_GROUP_ALLOWED_USERS",
|
||||
"TELEGRAM_GROUP_ALLOWED_USERS",
|
||||
"TELEGRAM_GROUP_ALLOWED_CHATS",
|
||||
"EMAIL_ALLOWED_USERS",
|
||||
"SMS_ALLOWED_USERS", "MATTERMOST_ALLOWED_USERS",
|
||||
"MATRIX_ALLOWED_USERS", "DINGTALK_ALLOWED_USERS",
|
||||
@@ -3411,8 +3413,11 @@ class GatewayRunner:
|
||||
Platform.QQBOT: "QQ_ALLOWED_USERS",
|
||||
Platform.YUANBAO: "YUANBAO_ALLOWED_USERS",
|
||||
}
|
||||
platform_group_env_map = {
|
||||
platform_group_user_env_map = {
|
||||
Platform.TELEGRAM: "TELEGRAM_GROUP_ALLOWED_USERS",
|
||||
}
|
||||
platform_group_chat_env_map = {
|
||||
Platform.TELEGRAM: "TELEGRAM_GROUP_ALLOWED_CHATS",
|
||||
Platform.QQBOT: "QQ_GROUP_ALLOWED_USERS",
|
||||
}
|
||||
platform_allow_all_map = {
|
||||
@@ -3469,27 +3474,36 @@ class GatewayRunner:
|
||||
|
||||
# Check platform-specific and global allowlists
|
||||
platform_allowlist = os.getenv(platform_env_map.get(source.platform, ""), "").strip()
|
||||
group_allowlist = ""
|
||||
group_user_allowlist = ""
|
||||
group_chat_allowlist = ""
|
||||
if source.chat_type in {"group", "forum"}:
|
||||
group_allowlist = os.getenv(platform_group_env_map.get(source.platform, ""), "").strip()
|
||||
group_user_allowlist = os.getenv(platform_group_user_env_map.get(source.platform, ""), "").strip()
|
||||
group_chat_allowlist = os.getenv(platform_group_chat_env_map.get(source.platform, ""), "").strip()
|
||||
global_allowlist = os.getenv("GATEWAY_ALLOWED_USERS", "").strip()
|
||||
|
||||
if not platform_allowlist and not group_allowlist and not global_allowlist:
|
||||
if not platform_allowlist and not group_user_allowlist and not group_chat_allowlist and not global_allowlist:
|
||||
# No allowlists configured -- check global allow-all flag
|
||||
return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes")
|
||||
|
||||
# Some platforms authorize group traffic by chat ID rather than sender ID.
|
||||
if group_allowlist and source.chat_type in {"group", "forum"} and source.chat_id:
|
||||
# Telegram can optionally authorize group traffic by chat ID.
|
||||
# Keep this separate from TELEGRAM_GROUP_ALLOWED_USERS, which gates
|
||||
# the sender user ID for group/forum messages.
|
||||
if group_chat_allowlist and source.chat_type in {"group", "forum"} and source.chat_id:
|
||||
allowed_group_ids = {
|
||||
chat_id.strip() for chat_id in group_allowlist.split(",") if chat_id.strip()
|
||||
chat_id.strip() for chat_id in group_chat_allowlist.split(",") if chat_id.strip()
|
||||
}
|
||||
if "*" in allowed_group_ids or source.chat_id in allowed_group_ids:
|
||||
return True
|
||||
|
||||
# Check if user is in any allowlist
|
||||
# Check if user is in any allowlist. In group/forum chats,
|
||||
# TELEGRAM_GROUP_ALLOWED_USERS is the scoped allowlist and should not
|
||||
# imply DM access; TELEGRAM_ALLOWED_USERS remains the platform-wide
|
||||
# allowlist and still works everywhere for backward compatibility.
|
||||
allowed_ids = set()
|
||||
if platform_allowlist:
|
||||
allowed_ids.update(uid.strip() for uid in platform_allowlist.split(",") if uid.strip())
|
||||
if group_user_allowlist:
|
||||
allowed_ids.update(uid.strip() for uid in group_user_allowlist.split(",") if uid.strip())
|
||||
if global_allowlist:
|
||||
allowed_ids.update(uid.strip() for uid in global_allowlist.split(",") if uid.strip())
|
||||
|
||||
@@ -3523,10 +3537,12 @@ class GatewayRunner:
|
||||
Resolution order:
|
||||
1. Explicit per-platform ``unauthorized_dm_behavior`` in config — always wins.
|
||||
2. Explicit global ``unauthorized_dm_behavior`` in config — wins when no per-platform.
|
||||
3. When an allowlist (``PLATFORM_ALLOWED_USERS`` or ``GATEWAY_ALLOWED_USERS``) is
|
||||
configured, default to ``"ignore"`` — the allowlist signals that the owner has
|
||||
deliberately restricted access; spamming unknown contacts with pairing codes
|
||||
is both noisy and a potential info-leak. (#9337)
|
||||
3. When an allowlist (``PLATFORM_ALLOWED_USERS``,
|
||||
``PLATFORM_GROUP_ALLOWED_USERS`` / ``PLATFORM_GROUP_ALLOWED_CHATS``,
|
||||
or ``GATEWAY_ALLOWED_USERS``) is configured, default to ``"ignore"`` —
|
||||
the allowlist signals that the owner has deliberately restricted
|
||||
access; spamming unknown contacts with pairing codes is both noisy
|
||||
and a potential info-leak. (#9337)
|
||||
4. No allowlist and no explicit config → ``"pair"`` (open-gateway default).
|
||||
"""
|
||||
config = getattr(self, "config", None)
|
||||
@@ -3565,8 +3581,18 @@ class GatewayRunner:
|
||||
Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOWED_USERS",
|
||||
Platform.QQBOT: "QQ_ALLOWED_USERS",
|
||||
}
|
||||
platform_group_env_map = {
|
||||
Platform.TELEGRAM: (
|
||||
"TELEGRAM_GROUP_ALLOWED_USERS",
|
||||
"TELEGRAM_GROUP_ALLOWED_CHATS",
|
||||
),
|
||||
Platform.QQBOT: ("QQ_GROUP_ALLOWED_USERS",),
|
||||
}
|
||||
if os.getenv(platform_env_map.get(platform, ""), "").strip():
|
||||
return "ignore"
|
||||
for env_key in platform_group_env_map.get(platform, ()):
|
||||
if os.getenv(env_key, "").strip():
|
||||
return "ignore"
|
||||
|
||||
if os.getenv("GATEWAY_ALLOWED_USERS", "").strip():
|
||||
return "ignore"
|
||||
|
||||
@@ -5,7 +5,14 @@ from unittest.mock import AsyncMock
|
||||
from gateway.config import Platform, PlatformConfig, load_gateway_config
|
||||
|
||||
|
||||
def _make_adapter(require_mention=None, free_response_chats=None, mention_patterns=None, ignored_threads=None):
|
||||
def _make_adapter(
|
||||
require_mention=None,
|
||||
free_response_chats=None,
|
||||
mention_patterns=None,
|
||||
ignored_threads=None,
|
||||
allow_from=None,
|
||||
group_allow_from=None,
|
||||
):
|
||||
from gateway.platforms.telegram import TelegramAdapter
|
||||
|
||||
extra = {}
|
||||
@@ -17,6 +24,10 @@ def _make_adapter(require_mention=None, free_response_chats=None, mention_patter
|
||||
extra["mention_patterns"] = mention_patterns
|
||||
if ignored_threads is not None:
|
||||
extra["ignored_threads"] = ignored_threads
|
||||
if allow_from is not None:
|
||||
extra["allow_from"] = allow_from
|
||||
if group_allow_from is not None:
|
||||
extra["group_allow_from"] = group_allow_from
|
||||
|
||||
adapter = object.__new__(TelegramAdapter)
|
||||
adapter.platform = Platform.TELEGRAM
|
||||
@@ -34,6 +45,7 @@ def _group_message(
|
||||
text="hello",
|
||||
*,
|
||||
chat_id=-100,
|
||||
from_user_id=111,
|
||||
thread_id=None,
|
||||
reply_to_bot=False,
|
||||
entities=None,
|
||||
@@ -50,10 +62,24 @@ def _group_message(
|
||||
caption_entities=caption_entities or [],
|
||||
message_thread_id=thread_id,
|
||||
chat=SimpleNamespace(id=chat_id, type="group"),
|
||||
from_user=SimpleNamespace(id=from_user_id),
|
||||
reply_to_message=reply_to_message,
|
||||
)
|
||||
|
||||
|
||||
def _dm_message(text="hello", *, from_user_id=111):
|
||||
return SimpleNamespace(
|
||||
text=text,
|
||||
caption=None,
|
||||
entities=[],
|
||||
caption_entities=[],
|
||||
message_thread_id=None,
|
||||
chat=SimpleNamespace(id=from_user_id, type="private"),
|
||||
from_user=SimpleNamespace(id=from_user_id),
|
||||
reply_to_message=None,
|
||||
)
|
||||
|
||||
|
||||
def _mention_entity(text, mention="@hermes_bot"):
|
||||
offset = text.index(mention)
|
||||
return SimpleNamespace(type="mention", offset=offset, length=len(mention))
|
||||
@@ -173,6 +199,68 @@ def test_config_bridges_telegram_group_settings(monkeypatch, tmp_path):
|
||||
assert __import__("os").environ["TELEGRAM_FREE_RESPONSE_CHATS"] == "-123"
|
||||
|
||||
|
||||
def test_config_bridges_telegram_user_allowlists(monkeypatch, tmp_path):
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
(hermes_home / "config.yaml").write_text(
|
||||
"telegram:\n"
|
||||
" allow_from:\n"
|
||||
" - \"111\"\n"
|
||||
" - \"222\"\n"
|
||||
" group_allow_from:\n"
|
||||
" - \"333\"\n"
|
||||
" group_allowed_chats:\n"
|
||||
" - \"-100\"\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.delenv("TELEGRAM_ALLOWED_USERS", raising=False)
|
||||
monkeypatch.delenv("TELEGRAM_GROUP_ALLOWED_USERS", raising=False)
|
||||
monkeypatch.delenv("TELEGRAM_GROUP_ALLOWED_CHATS", raising=False)
|
||||
|
||||
config = load_gateway_config()
|
||||
|
||||
assert config is not None
|
||||
assert __import__("os").environ["TELEGRAM_ALLOWED_USERS"] == "111,222"
|
||||
assert __import__("os").environ["TELEGRAM_GROUP_ALLOWED_USERS"] == "333"
|
||||
assert __import__("os").environ["TELEGRAM_GROUP_ALLOWED_CHATS"] == "-100"
|
||||
|
||||
|
||||
def test_config_env_overrides_telegram_user_allowlists(monkeypatch, tmp_path):
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
(hermes_home / "config.yaml").write_text(
|
||||
"telegram:\n"
|
||||
" allow_from: \"111\"\n"
|
||||
" group_allow_from: \"222\"\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "999")
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "888")
|
||||
|
||||
config = load_gateway_config()
|
||||
|
||||
assert config is not None
|
||||
assert __import__("os").environ["TELEGRAM_ALLOWED_USERS"] == "999"
|
||||
assert __import__("os").environ["TELEGRAM_GROUP_ALLOWED_USERS"] == "888"
|
||||
|
||||
|
||||
def test_dm_allow_from_is_enforced_by_gateway_authorization_not_trigger_gate():
|
||||
adapter = _make_adapter(allow_from=["111", "222"])
|
||||
|
||||
assert adapter._should_process_message(_dm_message("hello", from_user_id=111)) is True
|
||||
assert adapter._should_process_message(_dm_message("hello", from_user_id=333)) is True
|
||||
|
||||
|
||||
def test_group_allow_from_is_enforced_by_gateway_authorization_not_trigger_gate():
|
||||
adapter = _make_adapter(group_allow_from=["111"])
|
||||
|
||||
assert adapter._should_process_message(_group_message("hello", from_user_id=333)) is True
|
||||
|
||||
|
||||
def test_config_bridges_telegram_ignored_threads(monkeypatch, tmp_path):
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
|
||||
@@ -16,6 +16,8 @@ def _clear_auth_env(monkeypatch) -> None:
|
||||
"WHATSAPP_ALLOWED_USERS",
|
||||
"SLACK_ALLOWED_USERS",
|
||||
"SIGNAL_ALLOWED_USERS",
|
||||
"SIGNAL_GROUP_ALLOWED_USERS",
|
||||
"TELEGRAM_GROUP_ALLOWED_CHATS",
|
||||
"EMAIL_ALLOWED_USERS",
|
||||
"SMS_ALLOWED_USERS",
|
||||
"MATTERMOST_ALLOWED_USERS",
|
||||
@@ -178,9 +180,85 @@ def test_qq_group_allowlist_does_not_authorize_other_groups(monkeypatch):
|
||||
assert runner._is_user_authorized(source) is False
|
||||
|
||||
|
||||
def test_telegram_group_allowlist_authorizes_forum_chat_without_user_allowlist(monkeypatch):
|
||||
def test_telegram_group_user_allowlist_authorizes_forum_sender_without_dm_allowlist(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "-1001878443972")
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999")
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.TELEGRAM,
|
||||
GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}),
|
||||
)
|
||||
source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
user_id="999",
|
||||
chat_id="-1001878443972",
|
||||
user_name="tester",
|
||||
chat_type="forum",
|
||||
)
|
||||
|
||||
assert runner._is_user_authorized(source) is True
|
||||
|
||||
|
||||
def test_telegram_group_user_allowlist_rejects_other_senders(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999")
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.TELEGRAM,
|
||||
GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}),
|
||||
)
|
||||
source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
user_id="123",
|
||||
chat_id="-1001878443972",
|
||||
user_name="tester",
|
||||
chat_type="group",
|
||||
)
|
||||
|
||||
assert runner._is_user_authorized(source) is False
|
||||
|
||||
|
||||
def test_telegram_group_user_allowlist_wildcard_authorizes_any_sender(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "*")
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.TELEGRAM,
|
||||
GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}),
|
||||
)
|
||||
source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
user_id="123",
|
||||
chat_id="-1001878443972",
|
||||
user_name="tester",
|
||||
chat_type="group",
|
||||
)
|
||||
|
||||
assert runner._is_user_authorized(source) is True
|
||||
|
||||
|
||||
def test_telegram_group_user_allowlist_does_not_authorize_dms(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999")
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.TELEGRAM,
|
||||
GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}),
|
||||
)
|
||||
source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
user_id="999",
|
||||
chat_id="999",
|
||||
user_name="tester",
|
||||
chat_type="dm",
|
||||
)
|
||||
|
||||
assert runner._is_user_authorized(source) is False
|
||||
|
||||
|
||||
def test_telegram_group_chat_allowlist_authorizes_group_chat_without_user_allowlist(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_CHATS", "-1001878443972")
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.TELEGRAM,
|
||||
|
||||
Reference in New Issue
Block a user