From 45fce38b9edd426616a08ffcbadf866fa7070934 Mon Sep 17 00:00:00 2001 From: Nyaruko Date: Mon, 20 Jul 2026 09:24:45 +0800 Subject: [PATCH] fix(telegram): group authz fallback + command sender identity - authz_mixin: add config.extra fallback for group_allowed_chats when observe-unmentioned mode strips user_id from env-var check - authz_mixin: check adapter allow_from/group_allow_from for user authorization from config.yaml without env vars - telegram/adapter: separate group_allow_from for group chats vs allow_from for DMs - telegram/adapter: preserve sender source for command messages so admin-only slash commands work in groups - telegram/adapter: add _telegram_extra fallback for group_allow_from config reading --- gateway/authz_mixin.py | 33 +++++++++++++++++++++++++++ plugins/platforms/telegram/adapter.py | 19 +++++++++++---- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/gateway/authz_mixin.py b/gateway/authz_mixin.py index 884a60948c9c..7cd3dc1eb069 100644 --- a/gateway/authz_mixin.py +++ b/gateway/authz_mixin.py @@ -357,6 +357,24 @@ class GatewayAuthorizationMixin: if "*" in allowed_group_ids or source.chat_id in allowed_group_ids: return True + # Fallback: also check adapter-level config (config.yaml) + # for platforms..extra.group_allowed_chats. + # The Telegram observe-unmentioned mode strips user_id from + # triggered group messages (_apply_telegram_group_observe_attribution), + # so the env-var-only check above misses config.yaml-configured + # allowlists. Read the live adapter's config.extra as a fallback. + try: + adapter = self._adapter_for_source(source) + if adapter is not None: + extra = getattr(getattr(adapter, "config", None), "extra", None) or {} + adapter_group_allowed = extra.get("group_allowed_chats") + if adapter_group_allowed: + allowed = {str(c).strip() for c in adapter_group_allowed if str(c).strip()} + if "*" in allowed or source.chat_id in allowed: + return True + except Exception: + pass + # Bots admitted by {PLATFORM}_ALLOW_BOTS bypass the human allowlist (#4466). # Checked before the no-user-id guard below: some platforms deliver # bot/automation traffic with no user_id at all -- e.g. Slack Workflow @@ -525,6 +543,21 @@ class GatewayAuthorizationMixin: ) if effective_policy == "allowlist": return True + # Some adapters (e.g. Telegram) gate access via config.extra.allow_from / + # group_allow_from at intake but do not override enforces_own_access_policy. + # Check their allowlist here so config.yaml-configured allow_from works + # without requiring a separate {PLATFORM}_ALLOWED_USERS env var. + adapter = self._adapter_for_source(source) + if adapter is not None: + extra = getattr(getattr(adapter, "config", None), "extra", None) or {} + if source.chat_type in {"group", "forum"}: + adapter_allow = extra.get("group_allow_from") + else: + adapter_allow = extra.get("allow_from") + if adapter_allow: + allowed = {str(u).strip() for u in adapter_allow if str(u).strip()} + if user_id in allowed or "*" in allowed: + return True # No allowlists configured -- check global allow-all flag return _auth_env("GATEWAY_ALLOW_ALL_USERS").lower() in {"true", "1", "yes"} diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 09f97598e264..b4a46878e64e 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -1013,8 +1013,13 @@ class TelegramAdapter(BasePlatformAdapter): if not user_id: return True - # Adapter-level allow_from: when set, it is the sole authority. - adapter_allow_from = self.config.extra.get("allow_from") + # Adapter-level allow_from / group_allow_from: when set, they are the + # sole authority. Group chats use group_allow_from; DMs use allow_from. + chat_type = source.chat_type or "" + if chat_type in ("group", "forum"): + adapter_allow_from = self.config.extra.get("group_allow_from") + else: + adapter_allow_from = self.config.extra.get("allow_from") if adapter_allow_from is not None: allowed = {str(u).strip() for u in adapter_allow_from if str(u).strip()} return user_id in allowed or "*" in allowed @@ -7775,9 +7780,15 @@ class TelegramAdapter(BasePlatformAdapter): observe_prompt = self._telegram_group_observe_channel_prompt() channel_prompt = f"{event.channel_prompt}\n\n{observe_prompt}" if event.channel_prompt else observe_prompt if event.message_type == MessageType.COMMAND: + # Commands must retain the original source (with user_id) so + # slash-access control (_check_slash_access) can identify the + # sender. Replacing the source with an anonymised shared source + # (user_id=None) causes admin-only commands like /new to be + # denied even when the sender is an admin, because + # SlashAccessPolicy.is_admin(None) is always False. + # Still inject channel_prompt for group context. return dataclasses.replace( event, - source=shared_source, channel_prompt=channel_prompt, ) return dataclasses.replace( @@ -9394,7 +9405,7 @@ def _apply_yaml_config(yaml_cfg: dict, telegram_cfg: dict) -> dict | None: 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") + group_allowed_users = telegram_cfg.get("group_allow_from") or _telegram_extra.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)