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
This commit is contained in:
Nyaruko 2026-07-20 09:24:45 +08:00 committed by kshitij
parent 9eb7b1a6b1
commit 45fce38b9e
2 changed files with 48 additions and 4 deletions

View file

@ -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.<platform>.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"}

View file

@ -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)