feat(telegram): add allow_all_users/allowed_users to [telegram] config - #1297
Conversation
Discord and Slack have had allowed_users in their first-class config
sections for a while, but [telegram] never did - the only way to
restrict who can message a Telegram bot was the shared GATEWAY_*
env vars (GATEWAY_ALLOW_ALL_USERS/GATEWAY_ALLOWED_USERS), which
default to deny-all (per the identity-trust-none ADR) and aren't
Telegram-specific.
Add allow_all_users/allowed_users to TelegramConfig, matching the same
resolution convention already used for every other [telegram] field
and for Discord's allowed_users: config value wins, falls back to a
TELEGRAM_* env var (TELEGRAM_ALLOW_ALL_USERS / TELEGRAM_ALLOWED_USERS,
comma-separated), then auto-detects allow_all_users from whether the
resolved list is empty (same convention as [discord]/[slack]).
Wire the resolved values into main.rs's PlatformTrustConfigs registry
as a Telegram-specific override, mirroring exactly how Discord's own
block already overrides the shared GATEWAY_* L3 defaults - added
right after the existing Discord override.
Now a bot operator can write:
[telegram]
bot_token = "${TELEGRAM_BOT_TOKEN}"
allowed_users = ["176096071"]
instead of routing a plain user ID through spec.secrets/Secrets
Manager (oabctl's manifest has no plain env var field) or opening the
bot to allow_all_users=true just to admit one person.
Testing: cargo build/clippy -D warnings/test clean on macmini for the
whole workspace (openab-core: 48 tests incl. 5 new allowed_users
scenarios in telegram_resolve_all_scenarios; openab binary: 15 tests).
No regressions. Fixed one now-incomplete TelegramConfig struct literal
in an existing test (missing the two new fields).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Add the new [telegram] user access control fields to: - Unified mode env vars table - Full example config block - Field precedence table - Gateway Environment Variables table - New 'User Access Control' section with auto-detect logic explanation
7c45cce to
e6dc557
Compare
This comment has been minimized.
This comment has been minimized.
Align [telegram] user gating with the identity-trust-none ADR: - Empty allowed_users + no explicit flag → deny all (was: allow all) - Users must now explicitly set allow_all_users = true or list specific IDs in allowed_users to admit senders - Matches the shared GATEWAY_ALLOW_ALL_USERS default (false) This is not a breaking change for existing deployments because: - [telegram] config section is brand new (introduced in this PR) - Existing env-only deployments use GATEWAY_ALLOW_ALL_USERS which already defaults to deny-all Added Scenario 12 test: explicit allow_all_users = true opt-in.
This comment has been minimized.
This comment has been minimized.
…verride - F1: Resolve Telegram trust override even when [telegram] section is absent but TELEGRAM_ALLOWED_USERS / TELEGRAM_ALLOW_ALL_USERS env vars are set. Previously, env-only deployments (no config.toml section) silently ignored these env vars and fell back to GATEWAY_* defaults. - F2: Change TelegramConfig.allowed_users from Vec<String> (with #[serde(default)]) to Option<Vec<String>>. This lets serde distinguish omitted (None → fall back to env) from explicitly empty (Some([]) → deny all, config-authoritative). Fixes the case where allowed_users=[] in config.toml could not override a non-empty TELEGRAM_ALLOWED_USERS env var. - Add Scenario 13 test: explicit empty list overrides env var. Addresses review findings from 法師團隊 on PR #1297.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
CHANGES REQUESTED What This PR DoesAdds How It Works
Review Panel Verdict
Consensus: 2/3 LGTM, 1 CHANGES REQUESTED on regression risk + docs. Findings
Finding Details🟡 F1: Regression — shared GATEWAY_* silently overriddenProblem: When config.toml has a Impact: Fail-closed regression (bot unexpectedly rejects users). Severity is 🟡 not 🔴 because security posture becomes more restrictive, not less. Suggested fix: Only insert the override when Telegram-specific trust is actually configured: let has_telegram_trust_config = cfg.telegram.as_ref()
.map_or(false, |t| t.allow_all_users.is_some() || t.allowed_users.is_some())
|| std::env::var("TELEGRAM_ALLOW_ALL_USERS").is_ok()
|| std::env::var("TELEGRAM_ALLOWED_USERS").is_ok();
if has_telegram_trust_config {
let r = cfg.telegram.as_ref()
.map(|t| t.resolve())
.unwrap_or_else(|| config::TelegramConfig::default().resolve());
reg.insert("telegram", TrustConfig::new(
Some(allow_all_channels),
allowed_channels.clone(),
None,
Some(r.allow_all_users),
r.allowed_users,
));
}🟡 F2: PR description / implementation mismatchPR body states "empty list defaults to allow-all (same convention as [discord]/[slack])" but implementation correctly defaults to deny-all. Code is correct per identity-trust-none ADR; PR description should be updated. 🟡 F3: Cross-platform semantics divergenceDiscord uses 🟡 F4: Silent deny-all UXA fresh deployment with default config gets deny-all — bot silently ignores all messages. Docs should mention what the user experience looks like when denied and where to look for logs. 🟡 F5: Missing integration testThe Baseline Check
What's Good (🟢)
|
- TELEGRAM_ALLOW_ALL_USERS="" now resolves to false (deny-all) instead of true. Adds .filter(|v| !v.is_empty()) before parsing to prevent accidental fail-open from empty env var. - Added doc comment clarifying that allow_all_users=true bypasses the allowed_users list entirely. - Marked TELEGRAM_ALLOWED_USERS/TELEGRAM_ALLOW_ALL_USERS as "Unified binary only" in the Gateway env vars table — standalone gateway uses [gateway].allowed_users instead. - Added Scenario 14 test: empty-string env var must resolve to false.
|
LGTM ✅ — Clean, well-tested addition of per-platform What This PR DoesAdds first-class How It Works
Findings
Baseline Check
What's Good (🟢)
|
|
LGTM ✅ — All review findings addressed in What This PR DoesAdds How It Works
Findings
Finding Details🟡 F1: Empty-string env var fail-open (FIXED)
Fix: Added 🟡 F2: Supersede behavior undocumented (FIXED)When Fix: Added doc comment: "When this resolves to 🟡 F3: Gateway docs scope confusion (FIXED)The "Environment Variables (Gateway)" table included Fix: Added "Unified binary only — standalone gateway uses Baseline Check
Architecture Note: Platform Gating BoundaryOnce a What's Good (🟢)
|
Found while testing PR #1297 (telegram allowed_users): triggering PR Preview Build with variant=default builds openab with its default Cargo features (discord/slack/secrets-aws/agentcore/config-s3/ pre-seed) - telegram/line/feishu/wecom/googlechat/teams are NOT in that list, they only compile in via the 'unified' feature. The resulting preview image ran fine but had no webhook server at all for any of those platforms - every request returned 503 with no logs, since the whole #[cfg(feature = "telegram")]-gated code path was absent from the binary. Wasted a full deploy+test cycle before tracing it back to the Dockerfile's BUILD_MODE default. Add 'unified' as a variant choice, resolved to the same base Dockerfile as 'default' but with BUILD_MODE=unified passed as a docker build-arg, matching what the real release pipeline (build-operator.yml) uses via a different (Dockerfile.unified) mechanism for the multi-agent-variant builds - this workflow only supports the legacy single Dockerfile, so BUILD_MODE=unified is the correct way to get the same Cargo-feature set here. Produces the same ghcr.io/openabdev/openab:pr<N> tag as 'default' (same image name/suffix, different feature set baked in) - no new tag pattern introduced. Not run through CI (workflow_dispatch-only, can't be exercised by 'changes'/'check' path-filter jobs) - verified by re-triggering it for PR #1297 with variant=unified immediately after this commit and confirming the resulting image actually starts the telegram webhook server (see PR #1297 conversation for the live verification). Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Add a [line] config section for L3 identity trust, replacing the uniform GATEWAY_ALLOW_ALL_USERS/GATEWAY_ALLOWED_USERS env vars for LINE. First slice of #1355, mirroring the [telegram] pattern (#1297). - config.rs: LineConfig { allow_all_users, allowed_users } with LINE_ALLOW_ALL_USERS / LINE_ALLOWED_USERS env fallbacks and deny-all default (identity-trust-none ADR). Trust-only by design — channel credentials stay on LINE_CHANNEL_SECRET / LINE_CHANNEL_ACCESS_TOKEN. - main.rs: [line] (or LINE_* env) overrides the uniform GATEWAY_* seed in the trust registry, exactly like the telegram override. When LINE is active and still driven by the legacy GATEWAY_* env, log a Phase 1 deprecation warning (becomes an error in Phase 2, #1356). - docs: config.toml.example, config-reference.md ([line] section), line.md (User Trust section + env table). - tests: TOML parse + all env resolution scenarios in one test fn (env-race safety, same pattern as telegram_resolve_all_scenarios). Group policy (open/members) and Reply-API deny-echo are follow-ups on the issue. Refs #1355, umbrella #1356, ADR #1291. Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…refs The design shipped across #1356/#1375 (#1297, #1365/#1366 trust slices; #1381-#1385 full config-first parity). Corrections vs implementation: googlechat field names (sa_key_json + audience), Teams sender-id is the Bot Framework activity.from.id (not AAD Object ID). The [gateway] section deprecation warning is deferred to Phase 1c (the WS two-process model still uses [gateway] legitimately) — tracked on #1356. Merging also fixes the dangling first-class-platform-config.md link in the already-merged identity-trust-none ADR.
* docs(adr): first-class per-platform config & trust-none default Propose promoting all gateway-connected platforms (Telegram, LINE, Feishu, WeCom, Google Chat, MS Teams) to top-level config sections, matching the existing [discord] and [slack] structure. Key decisions: - Per-platform [telegram], [line], [feishu], etc. sections - Trust-none default (empty allowed_users = deny all) - Single trust gate at AdapterRouter::handle_message() - Echo sender ID on deny - Deprecate [gateway] catch-all section Tracking: #1262 * docs(adr): add trust pyramid with platform auth comparison table Layer 1 (gateway): Platform authentication mechanisms per adapter - Telegram: secret token + IP range - LINE: HMAC-SHA256 - Feishu: SHA256 signature + encrypt key - WeCom: token signature + AES decrypt - Google Chat: JWT (RS256 via JWKS) - MS Teams: JWT (OpenID Connect) - Slack/Discord: WebSocket token auth Layer 2 (core): Channel/group trust (existing) Layer 3 (core): User trust (this ADR - flip to deny-all) * docs(adr): refine trust pyramid — L2 scope (open default) vs L3 identity (deny default) Clarify the three layers per review discussion: - L1 platform auth (security, edge) - L2 channel/group/DM scope control — NOT security, default OPEN; the platform already enforces channel membership, so L2 is operator scoping - L3 identity trust — THE security gate, default DENY-ALL, covers all paths - allow_dm is an L2 surface toggle; DMs have no platform membership gate so L3 is their sole protection - L2 must stay open by default for the echo-UID request-access flow to work * docs(adr): specify trait & type changes — extend carriers, no new trait - Extend TrustConfig with L2 scope fields (allow_all_channels, allow_dm) + surface_allowed(); defaults L2-open / L3-deny - Add 'Trait & Type Changes' section: pass SenderContext in MessageContext, add is_dm to ChannelRef, no new ChatAdapter method/trait (uniform logic) - Note the real refactor = remove scattered trust checks from discord.rs/slack.rs/gateway.rs so the router gate is un-bypassable - Fix architecture diagram gate labels (L2 optional/open, L3 deny default) * docs(adr): sharpen title to 'identity trust-none default' * docs(adr): scope down to per-platform config only Split the trust/security decision out into a separate ADR (docs/adr/identity-trust-none.md, PR #1264). This ADR now covers only the config schema change: first-class [platform] sections + [gateway] deprecation + migration. * docs(adr): mark first-class platform config as Accepted with shipped refs The design shipped across #1356/#1375 (#1297, #1365/#1366 trust slices; #1381-#1385 full config-first parity). Corrections vs implementation: googlechat field names (sa_key_json + audience), Teams sender-id is the Bot Framework activity.from.id (not AAD Object ID). The [gateway] section deprecation warning is deferred to Phase 1c (the WS two-process model still uses [gateway] legitimately) — tracked on #1356. Merging also fixes the dangling first-class-platform-config.md link in the already-merged identity-trust-none ADR. --------- Co-authored-by: 超渡法師 <chaodu-agent@openab.dev> Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
#1391) * feat(gateway)!: route standalone WS path through the shared trust gate Phase 1c prerequisite (#1356): the standalone-gateway WebSocket path (run_gateway_adapter) now enforces L2 (channel scope) and L3 (identity) via the same per-platform trust registry as the unified path, instead of its own inline allowlist checks. - gateway.rs: extract gate_gateway_event — the shared ingress gate (gate_incoming + DenyIdentity throttled request-access echo + DenyScope silent drop) used by BOTH process_gateway_event (unified) and the WS loop. Gate runs before slash handling so untrusted senders cannot execute /reset|/cancel|/config. should_skip_event on the WS path is neutered to structural gating only (bot admission + @mention), matching the unified path. - main.rs: seed the registry with the [gateway] section's trust for its platform (gateway_section_trust — same resolve_allow_all semantics the old inline filter used). Precedence: GATEWAY_* env (uniform seed) < [gateway] section < [<platform>] section (platform_trust_override). - Dead code removed: GatewayParams and GatewayEventContext channel/user allowlist fields (unified ones were already unread post-#1297; WS ones move to the registry), plus the unified block's GATEWAY_ALLOW_ALL_* env reads (the registry's uniform seed already consumes them). - Docs: the six mode-scoping callouts (line/wecom/google-chat/teams/ config-reference ×2) now describe the consolidated trust resolution. BREAKING CHANGE: in standalone-gateway deployments that define BOTH [gateway] allowlists AND a first-class [<platform>] trust section, the platform section now wins on the WS path (documented target state: config-first, platform section is the most specific). Previously the platform section had no effect on that path. Migration: if your WS deployment relies on [gateway].allowed_users while also carrying a [<platform>] section with different values, align the platform section (it is now authoritative for that platform). Tests: gateway_section_trust_mirrors_ws_filter_semantics + gateway_trust_seed_precedence_env_lt_gateway_lt_platform (main.rs) + ws_path_filter_is_structural_only (gateway.rs). Refs #1356. * fix(gateway): deny-echo must not be awaited inside the WS event loop Self-review catch: the gate helper awaited send_message inline in the WS select arm. In streaming mode send_gateway_reply registers a pending request and waits (5s timeout) for a GatewayResponse — which is dispatched by that same loop arm. Every DenyIdentity would therefore stall ALL event processing for the full reply timeout; distinct untrusted senders on a public bot could chain these stalls into a DoS. Restructure: gate_gateway_event is now a sync decision helper returning GateOutcome { Allow | Deny { echo } } — the caller chooses delivery. The WS path spawns the echo onto the JoinSet (never blocking the loop, matching the existing fire-and-forget pattern for slash responses); the unified path (axum/bridge task context) awaits it directly as before. Rationale documented on the enum and both call sites. --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Summary
[discord]/[slack]have hadallowed_usersin their first-class configsections for a while;
[telegram]never did. The only way to restrict whocan message a Telegram bot today is the shared
GATEWAY_ALLOW_ALL_USERS/GATEWAY_ALLOWED_USERSenv vars — which default to deny-all (per theidentity-trust-none ADR) and aren't Telegram-specific in name or intent.
Came up directly from deploying a real bot via
oabctl— the manifestschema has no plain (non-secret) env var field, only
spec.secrets, so therewas no clean way to admit a single Telegram user ID without routing it
through Secrets Manager or opening the bot to everyone.
Change
Adds
allow_all_users/allowed_usersto[telegram], matching the exactresolution convention already used for every other
[telegram]field andfor
[discord].allowed_users:TELEGRAM_ALLOW_ALL_USERS/TELEGRAM_ALLOWED_USERS(comma-separated) env vars → auto-detectsallow_all_usersfrom whether the resolved list is empty (same conventionas
[discord]/[slack]: non-empty list defaults to deny-all-except-list,empty list defaults to allow-all).
main.rs'sPlatformTrustConfigsregistry as aTelegram-specific override, right after the existing Discord override —
same pattern, same place.
Testing done
5 new scenarios added to
telegram_resolve_all_scenarios(the existingconsolidated env-var test, kept serialized to avoid
std::envraceconditions under parallel test execution — matches the existing test's own
convention): config list wins over env, empty-list auto-detect → allow-all,
non-empty-list auto-detect → deny-all-except-list,
TELEGRAM_ALLOWED_USERSenv fallback (comma-separated + trimmed), explicit
allow_all_users = falseoverride.
Also fixed one now-incomplete
TelegramConfigstruct literal in an existingtest (missing the two new fields, caught immediately by the compiler).
Verified on macmini per the project's build-offloading convention, not run
locally.