fix(discord): check pairing store for component button auth

Component button interactions (approve/deny, slash confirm, model
picker, clarify) were not checking the pairing store for authorization.
Users approved via `hermes pairing approve` could send messages and use
slash commands (which go through the gateway authz_mixin), but button
clicks were rejected because `_component_check_auth` only checked
env-var allowlists (DISCORD_ALLOWED_USERS, GATEWAY_ALLOW_ALL_USERS,
etc.) and not the pairing store.

This was a regression from commit f6f363662 which intentionally made
component auth fail-closed when no allowlist is set (security fix for
GHSA-mc26-p6fw-7pp6), but did not account for pairing-based auth.

Fix: add a `PairingStore.is_approved("discord", uid)` check to
`_component_check_auth`, mirroring `authz_mixin._check_authorization`.
The pairing store check runs after all allowlist checks, preserving the
fail-closed behavior for non-paired, non-allowed users.

Fixes #50627
This commit is contained in:
liuhao1024 2026-06-22 13:36:41 +08:00 committed by Teknium
parent 0957d77187
commit 7ff48a6291
2 changed files with 66 additions and 5 deletions

View file

@ -5709,7 +5709,8 @@ def _component_check_auth(
Mirrors the gateway's external-surface authorization model: component
button clicks must be explicitly authorized by a Discord user/role
allowlist, a global user allowlist, or an explicit allow-all flag.
allowlist, a global user allowlist, an explicit allow-all flag, or
the pairing store (``hermes pairing approve``).
Behavior:
@ -5719,6 +5720,7 @@ def _component_check_auth(
- role allowlist set + interaction.user has no resolvable
``roles`` attribute (e.g. DM context with a role policy active)
-> reject (fail closed)
- user is approved in the pairing store -> allow
- otherwise -> reject
"""
if os.getenv("DISCORD_ALLOW_ALL_USERS", "").strip().lower() in {"true", "1", "yes"}:
@ -5740,11 +5742,13 @@ def _component_check_auth(
if user is None:
return False
# Resolve user ID once for both allowlist and pairing checks.
try:
uid = str(user.id)
except AttributeError:
uid = ""
if has_users:
try:
uid = str(user.id)
except AttributeError:
uid = ""
if "*" in user_set or (uid and uid in user_set):
return True
@ -5763,6 +5767,18 @@ def _component_check_auth(
if user_role_ids & role_set:
return True
# Check pairing store — mirrors ``authz_mixin._check_authorization``
# so users approved via ``hermes pairing approve`` can interact with
# component buttons even without DISCORD_ALLOWED_USERS set.
if uid:
try:
from gateway.pairing import PairingStore
store = PairingStore()
if store.is_approved("discord", uid):
return True
except Exception:
pass
return False