Skip to content

Add WHATSAPP_GROUP_ALLOWED_USERS group allowlist support for WhatsApp gateway - #8278

Open
RiptideV wants to merge 2 commits into
NousResearch:mainfrom
RiptideV:add-whatsapp-group-allowed-users
Open

RiptideV wants to merge 2 commits into
NousResearch:mainfrom
RiptideV:add-whatsapp-group-allowed-users

Conversation

@RiptideV

@RiptideV RiptideV commented Apr 12, 2026

Copy link
Copy Markdown

What does this PR do?

Adds WHATSAPP_GROUP_ALLOWED_USERS group allowlist support like SIGNAL_GROUP_ALLOWED_USERS

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

How to Test

  1. Setup Whatsapp Gateway
  2. Add hermes to a group with unauthenticated users
  3. Add group to WHATSAPP_GROUP_ALLOWED_USERS
  4. Test if hermes responds to unauthenticated users in the group
  5. Test that unauthenticated users from the group cannot DM hermes

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

@RiptideV
RiptideV force-pushed the add-whatsapp-group-allowed-users branch 3 times, most recently from a6ebe4e to aa4215f Compare April 12, 2026 22:02
@RiptideV
RiptideV marked this pull request as ready for review April 12, 2026 22:04
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists platform/whatsapp WhatsApp Business adapter comp/gateway Gateway runner, session dispatch, delivery labels Apr 28, 2026
@razorglyon

Copy link
Copy Markdown

Hi @RiptideV — running into this exact use case (community manager bot replying to non-admin members in an allowlisted WhatsApp group), so very interested in this PR landing.

I think there may be a coverage gap that's worth surfacing before merge, since I think it impacts the documented testing flow:

The gateway-level _is_user_authorized() change is exactly what's needed once a message reaches Python. However, in bot mode, the Node bridge applies its own sender-level filter first at scripts/whatsapp-bridge/bridge.js:287:

if (!msg.key.fromMe) {
  if (WHATSAPP_MODE === 'self-chat') {
    // ignored: self_chat_mode_rejects_non_self
    continue;
  }
  if (!matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {
    // ignored: allowlist_mismatch
    continue;
  }
}

ALLOWED_USERS is built only from WHATSAPP_ALLOWED_USERS (line 53), with no awareness of WHATSAPP_GROUP_ALLOWED_USERS, WHATSAPP_ALLOW_ALL_USERS, or whether chatId is a @g.us group in the allowlist. So when WHATSAPP_ALLOWED_USERS is set to a finite list (e.g. for DM hardening), every non-admin group member gets dropped by the bridge before the gateway can apply this PR's new group authorization.

Concrete evidence from my own bridge log: every recent event from an allowlisted @g.us chat (9/9 in the window where members were active) was logged as allowlist_mismatch, with zero events forwarded to the gateway. Reproducible as long as the senders aren't also in WHATSAPP_ALLOWED_USERS.

Two possible workarounds at the bridge level both have downsides:

So in practice this PR's group allowlist only takes effect if operators are willing to drop their bridge-level DM allowlist, which seems counter to the intent.

Two ways to close this:

  1. Document explicitly in whatsapp.md that WHATSAPP_GROUP_ALLOWED_USERS requires WHATSAPP_ALLOWED_USERS=* and that DM hardening must move to a separate mechanism. Smaller patch, but the security tradeoff is significant.
  2. Teach the bridge about WHATSAPP_GROUP_ALLOWED_USERS (and probably WHATSAPP_ALLOW_ALL_USERS for consistency) — bypass the sender check at line 287 when isGroup && allowedGroups.has(chatId). Keeps DM hardening at the bridge layer (its current first-line-of-defense role) while letting group allowlist compose orthogonally with it. Happy to put up a complementary PR for bridge.js + allowlist.js (with parity tests) if (2) is the preferred direction — would keep this PR's scope clean.

Thanks for pushing this — broadly I think (2) is the cleaner mental model, but happy to follow whichever direction the maintainers prefer.

@RiptideV
RiptideV force-pushed the add-whatsapp-group-allowed-users branch from aa4215f to ab741da Compare May 16, 2026 20:32
@aldoeliacim

Copy link
Copy Markdown
Contributor

Ran into this exact gap in production and wanted to share a finding that I think is orthogonal to (and complementary with) this PR's approach — it's about a second authorization gate that an env-var-only fix leaves uncovered.

The blind spot: config-driven group_allow_from never reaches the gateway gate

WHATSAPP_GROUP_ALLOWED_USERS (this PR) and the config→env bridge both assume the allowlist arrives as an env var. But the bridge in gateway/config.py only mirrors a top-level whatsapp: YAML key — it does not read platforms.whatsapp.extra.group_allow_from, which is the documented platforms-style config shape. So an operator who configures:

platforms:
  whatsapp:
    extra:
      group_policy: allowlist
      group_allow_from:
        - "<group-jid>@g.us"

gets group_allow_from resolved by the adapter (WhatsAppAdapter._group_allow_from) and enforced at intake — but the env var stays empty, so the gateway-layer _is_user_authorized check this PR adds silently authorizes nothing for that config. With a populated WHATSAPP_ALLOWED_USERS (per-user DM allowlist), every group sender who isn't also on the per-user list is then rejected at the gateway after the adapter already admitted the group — so only DM-allowlisted users can invoke the bot in an allowlisted group, even when another member @-mentions it directly.

What worked for us

Resolve the group allowlist from both sources at the gateway gate, mirroring the existing _adapter_dm_policy live-adapter-read pattern:

def _whatsapp_group_allowlist(self) -> set:
    groups = set()
    raw_env = os.getenv("WHATSAPP_GROUP_ALLOWED_USERS", "").strip()
    if raw_env:
        groups.update(g.strip() for g in raw_env.split(",") if g.strip())
    adapter = (getattr(self, "adapters", None) or {}).get(Platform.WHATSAPP)
    adapter_groups = getattr(adapter, "_group_allow_from", None) if adapter else None
    if adapter_groups:
        groups.update(str(g).strip() for g in adapter_groups if str(g).strip())
    # (+ config.extra fallback for bare runners without a live adapter)
    return groups

Then the chat-scoped check normalizes both sides (so a group listed bare or with the @g.us suffix both match) and authorizes any member of a listed group.

Security invariant we verified

Group authorization is chat-scoped and must not grant DM access — a user only reachable via an allowlisted group is still rejected in a DM (the per-user DM allowlist runs unchanged below the group block), and since pairing is DM-only, an unauthorized group sender is silently ignored rather than offered a pairing code. We cover this with a regression test (test_group_allowlist_does_not_grant_dm) plus the adapter-config path (test_group_authz_reads_adapter_config_when_env_empty).

Happy to send the _group_allow_from adapter-read piece as a follow-up commit on top of this PR if that's useful — it composes cleanly with the env-var path here and closes the platforms.*.extra case. Mirrors the TELEGRAM_GROUP_ALLOWED_CHATS / SIGNAL_GROUP_ALLOWED_USERS chat-scoped precedent.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling the WhatsApp group/DM split. The underlying issue remains real on current main: scripts/whatsapp-bridge/bridge.js:637 applies the finite DM allowlist before Python sees group traffic, while the adapter already supports independent group_policy / group_allow_from at plugins/platforms/whatsapp/adapter.py:409-412.

Problems

  • The PR's gateway hunk targets the pre-refactor gateway/run.py; current authorization is in gateway/authz_mixin.py:264. Its per-user WHATSAPP_ALLOWED_USERS check at gateway/authz_mixin.py:561-585 also needs the WhatsApp group-scoped exception, or bridge-admitted group traffic is rejected by the second gate.
  • whatsapp.group_allow_from is the current canonical config path and is exported to WHATSAPP_GROUP_ALLOWED_USERS at plugins/platforms/whatsapp/adapter.py:1738-1742; the additional group_allowed_users alias should not create a competing configuration surface.

Suggested changes

  • Port the bridge group bypass and the group-scoped gateway authorization into the current files, keeping the grant group-only so it never authorizes a DM.
  • Add an integration regression for finite DM allowlists plus an allowlisted group, including the DM-denial case.

Automated hermes-sweeper review.

Comment thread gateway/config.py
if isinstance(gaf, list):
gaf = ",".join(str(v) for v in gaf)
os.environ["WHATSAPP_GROUP_ALLOWED_USERS"] = str(gaf)
gac = whatsapp_cfg.get("group_allowed_users")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not add group_allowed_users as a second WhatsApp config key. Current main uses canonical whatsapp.group_allow_from, and plugins/platforms/whatsapp/adapter.py:1738-1742 already exports it to WHATSAPP_GROUP_ALLOWED_USERS; port the behavior through that existing path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/whatsapp WhatsApp Business adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants