Repository navigation
fix(discord): read allowed_guilds as IDs and deny every guild on an unreadable allowlist - #6338
Conversation
…he bot config.example.yaml documents channels.discord.allowed_guilds as a value that "can also be a single guild ID", but the constructor iterates it: a scalar string becomes one entry per character, and an unquoted YAML int raises TypeError. Whatever remains is gated by truthiness, so an allowlist with no parseable entry leaves the set empty and admits every guild.
…able
The allowlist was built by iterating the configured value, so the documented
single-guild shorthand ("can also be a single guild ID" in
config.example.yaml) became one entry per character: allowed_guilds: "321"
admitted guilds 1, 2 and 3 while blocking 321, and an unquoted YAML int raised
TypeError in the constructor. The empty result then read as "no allowlist", so
a restriction the operator configured could open the bot to every guild the bot
joined.
Parse both ID lists through helpers that treat a scalar as a one-entry list and
drop unreadable entries with a warning, following the telegram channel: an
allowlist with no parseable entry now denies every guild and logs at ERROR.
Mention exemptions keep their IDs verbatim, where an unreadable entry only
leaves mention_only in force.
willem-bd
left a comment
There was a problem hiding this comment.
Reviewed against 8f6b662. The parser is correct and mirrors the merged Telegram precedent (_parse_allowed_users, #6230): None means no allowlist, an empty frozenset means deny-all, and _check_guild replaces the truthiness test. I ran backend/tests/test_discord_channel.py in a clean worktree at this head (import path verified to resolve to the worktree): 53 passed; ruff check and ruff format --check are clean on both files. The fail-closed change for an unreadable allowlist is deliberate, tested, and the right call. One suggestion below: the operator-facing comments for these keys in config.example.yaml were not updated alongside the behavior change.
| - ``bot_token``: Discord Bot token. | ||
| - ``allowed_guilds``: (optional) List of allowed Discord guild IDs. Empty = allow all. | ||
| - ``allowed_guilds``: (optional) List of allowed Discord guild IDs, or a | ||
| single ID. Empty = allow all; a non-empty list with no valid ID denies |
There was a problem hiding this comment.
Docs sync: the operator-facing template comments for these keys still describe the old behavior. In config.example.yaml (line 3042 at this head) the allowed_guilds comment reads "empty = allow all guilds; can also be a single guild ID" — it does not mention that a non-empty value yielding no usable ID now denies every guild, which is the default behavior change this PR declares (its own "Config / setup" and "Docs" surface boxes are unchecked). The allowed_channels comment two lines below likewise does not mention the single-ID shorthand this PR adds. Since config.example.yaml is the primary operator doc for channels.discord, please update both comments in this change set per the repo's docs-update policy.
Fixes #6337
Why
config.example.yaml:3042tells operators thatchannels.discord.allowed_guilds"can also be a single guild ID", butDiscordChannel.__init__builds the allowlist by iterating the configured value. A scalar therefore becomes one entry per character, and a value the parser cannot read at all becomes an empty set — which the inbound gate then reads as "no allowlist".So an operator who restricts the bot to their own server either blocks that server and admits three unrelated ones, or, with a value like
"@acme"or a{id: …}mapping from the runtime channel config, opens the bot to every guild it has joined. Nothing is logged either way. Fixes #6337.What changed
allowed_guildsaccepts the documented shorthand: a single ID — quoted or not — is one entry, not its digits.allowed_guilds: 321no longer raisesTypeError: 'int' object is not iterablewhile the channel is being constructed.WARNING, naming the entry) instead of dropped by a barecontinue.ERROR, instead of silently allowing all. The operator asked for a restriction; an unreadable one no longer opens the bot. Leaving the key unset, or[], still means allow all.allowed_channels(themention_onlyexemption list), parsed by the same lines, gets the same scalar handling. Its IDs are kept verbatim — an unreadable entry there only leavesmention_onlyin force, which is already fail-closed.The shape follows the merged Telegram counterpart,
_parse_allowed_users(#6230):Nonemeans "no allowlist", an empty frozenset means "deny all", and the gate is a small_check_guild()predicate rather than a truthiness test on the set.Surface area
backend/app/channels/discord.pyallowed_guildsnow denies every guild rather than allowing all; the log line tells the operator to fix the value. Unset/[]is unchanged.Screenshots / Recording
N/A — no frontend change.
Bug fix verification
backend/tests/test_discord_channel.py::TestDiscordAllowedGuildsand::TestDiscordAllowedChannels(26 cases, added in the first commit).main, green on this branch: yes.The two passes on
mainare the negative controls ("322","457"— values that must deny). Red covers both shapes measured onmain:TestDiscordAllowedGuilds::test_on_message_applies_the_allowlistandTestDiscordAllowedChannels::test_exempt_channel_needs_no_mentiondrive the real_on_messagepath with the documented scalar, so the assertion is about admission, not about the parsed set.Validation
pytest tests/test_discord_channel.py— 53 passed.pytest tests/test_channels.py— 413 passed, 2 errors. Both errors are pre-existing and Windows-only: a CJK-parametrizedtest_split_text_preserves_content_within_discord_limitid makes pytest's ownPYTEST_CURRENT_TESTexceed the 32767-characteros.environlimit during teardown. Reproduced identically (9 passed, 2 errors) at5872d07ewith this branch's changes reverted, so it is not caused by this diff, and CI runs Ubuntu.ruff check+ruff format --diffover both files — clean.make test(full backend suite) and the blocking-I/O suite, which need the repo'suv-managed dev group on Linux; this change touches only config parsing in one channel constructor and its inbound gate.AI assistance
Tool(s) used: Qoder (agent session on this account).
How was it used: locating the parsing site and the
config.example.yamlpromise, writing the failing tests first, writing the patch, and running the tests/lint above. The measured red/green numbers and the pre-existing Windows errors are reproduced verbatim from the runs.Has a human read every line of this diff? Not yet — left unchecked deliberately; the account owner has not reviewed this diff line by line at the time of filing. Tick this box once they have.