From 9e850fedf0cd08a2d5994cebddb2c62dcabe2c09 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Mon, 5 Oct 2026 01:57:15 -0400 Subject: [PATCH] Discord perms: partial guild payload is unknown, not a deny (#225 S1 review) A guild without its @everyone role (or that role's permissions) was read as base 0, a definite deny that would block a start. It now raises ValueError, which S3 treats as unknown and lets the start go ahead. Co-Authored-By: Claude Opus 5.5 --- drb-c2-core/app/internal/discord_perms.py | 9 +++++++-- drb-c2-core/tests/test_discord_perms.py | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/drb-c2-core/app/internal/discord_perms.py b/drb-c2-core/app/internal/discord_perms.py index e2ae2da..d248114 100644 --- a/drb-c2-core/app/internal/discord_perms.py +++ b/drb-c2-core/app/internal/discord_perms.py @@ -178,9 +178,14 @@ def effective_permissions( if member_id and member_id == _sid(guild.get("owner_id")): return ALL_PERMISSIONS, {} - # 2. Base from @everyone + the member's roles. + # 2. Base from @everyone + the member's roles. Every guild has an + # @everyone role (id == guild id) with a permissions value; without one + # the payload is partial, and reading it as "no permissions" would turn + # it into a definite deny instead of "unknown" (the plan is fail-open). everyone_role = roles.get(guild_id) - base = parse_permissions(everyone_role.get("permissions")) if everyone_role else 0 + if not guild_id or everyone_role is None or everyone_role.get("permissions") in (None, ""): + raise ValueError(f"guild payload has no @everyone role permissions (guild {guild_id or '?'})") + base = parse_permissions(everyone_role.get("permissions")) for rid in member_role_ids: role = roles.get(rid) if role is not None and rid != guild_id: diff --git a/drb-c2-core/tests/test_discord_perms.py b/drb-c2-core/tests/test_discord_perms.py index 3593748..81215ed 100644 --- a/drb-c2-core/tests/test_discord_perms.py +++ b/drb-c2-core/tests/test_discord_perms.py @@ -245,9 +245,21 @@ def test_unknown_member_role_ids_are_ignored(): assert bits == VOICE -def test_missing_everyone_role_means_empty_base(): - g = {"id": GUILD, "owner_id": OWNER, "roles": []} - bits, trace = eff(g, member(), channel()) +@pytest.mark.parametrize("g", [ + {"id": GUILD, "owner_id": OWNER, "roles": []}, + {"id": GUILD, "owner_id": OWNER}, + {"id": GUILD, "owner_id": OWNER, "roles": [{"id": GUILD, "name": "@everyone"}]}, + {"owner_id": OWNER, "roles": [{"id": GUILD, "name": "@everyone", "permissions": "0"}]}, +]) +def test_partial_guild_payload_raises_not_silently_denies(g): + # A guild without its @everyone role (or its permissions) is a partial + # payload: "unknown" (ValueError, start goes ahead), never a definite deny. + with pytest.raises(ValueError): + eff(g, member(), channel()) + + +def test_everyone_role_granting_nothing_is_a_real_deny(): + bits, trace = eff(guild(everyone=0), member(), channel()) assert bits == 0 and "not granted" in trace["View Channel"]