Files
hermes-agent/tests/hermes_cli/test_web_server_messaging_posture.py
Austin Pickett e93446126d fix(dashboard): surface the messaging platform's auth posture in the Test result
The Channels 'Test' button stopped at connectivity: a platform whose gateway
authorization posture denies every sender (no allowlists, no allow-all flag, no
pairing grants) still answered '<Platform> is connected.' — a green checkmark
on a bot that ignores everyone (#66030).

When the adapter reports connected, the test result now carries the platform's
access posture for the ACTIVE profile, mirroring the decision ladder of
AuthorizationMixin._principal_authorized:

- open: {PLATFORM}_ALLOW_ALL_USERS / GATEWAY_ALLOW_ALL_USERS
- allowlist: {PLATFORM}_ALLOWED_USERS, GATEWAY_ALLOWED_USERS, the Telegram
  group allowlist vars, or a config-enforced dm_policy: allowlist
- pairing: approved pairing grants in the profile's pairing store
- deny_all: nothing configured — with pending pairing requests counted in
  the message

The check is profile-aware: under a named profile it reads that profile's
.env/config/pairing store, never the dashboard process's os.environ (which
carries the ROOT install's credentials). ok stays True (the connection is up);
the new auth_posture field lets the UI render yellow instead of green.

Reuses the idea from PR #66055 (authored by @webtecnica) with the evaluator
made profile-aware and extended to cover GATEWAY_ALLOWED_USERS, pairing grants
and adapter/config-enforced allowlists, per its review.

Fixes #66030
2026-10-06 12:28:06 -04:00

188 lines
7.9 KiB
Python

"""The Channels "Test" button must answer the auth-posture question too (#66030).
``POST /api/messaging/platforms/{platform_id}/test`` used to stop at connectivity:
a Telegram bot whose gateway denies every sender (no allowlists, no allow-all flag,
no pairing grants) still answered ``ok: true, "Telegram is connected."`` — a green
checkmark on a bot that ignores everyone. The test result now carries the platform's
access posture for the ACTIVE profile (allowlist / open / pairing / deny_all),
counting pairing grants, evaluated against the profile's own env/config/pairing
store — never the dashboard process's environment.
"""
import json
import os
from pathlib import Path
import pytest
_VALID_BOT_TOKEN = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_1234"
def _gateway_state(home: Path) -> None:
"""A live gateway record: this pytest process's PID wearing a gateway command
line, with the Telegram adapter reported ``connected``."""
import gateway.status as _gw_status
start_time = _gw_status._get_process_start_time(os.getpid())
(home / "gateway.pid").write_text(
json.dumps({"pid": os.getpid(), "hermes_home": str(home)}), encoding="utf-8"
)
(home / "gateway_state.json").write_text(json.dumps({
"pid": os.getpid(), "hermes_home": str(home), "gateway_state": "running",
"kind": "hermes-gateway", "start_time": start_time,
"updated_at": "2026-01-01T00:00:00+00:00",
"platforms": {"telegram": {"state": "connected", "writer_pid": os.getpid()}},
}), encoding="utf-8")
@pytest.fixture
def client(monkeypatch, _isolate_hermes_home):
from starlette.testclient import TestClient
import hermes_state
import hermes_constants
import gateway.status as _gw_status
from hermes_constants import get_hermes_home
from hermes_cli.web_server import app, _SESSION_HEADER_NAME, _SESSION_TOKEN
home = get_hermes_home()
monkeypatch.setattr(hermes_state, "DEFAULT_DB_PATH", home / "state.db")
monkeypatch.setattr(hermes_constants, "_default_hermes_root_memo", None)
# Liveness is a verified identity: this pytest process passes as the gateway
# only while its probed command line matches the home being asked about
# (``_command_line_belongs_to_profile``). The worker test below re-points the
# faked command line at ``-p worker`` before its request.
cmdline = {"current": "hermes gateway run"}
monkeypatch.setattr(
_gw_status, "_read_process_cmdline", lambda pid: cmdline["current"]
)
# The dashboard process's own env must never authorize anyone here.
for var in ("TELEGRAM_BOT_TOKEN", "TELEGRAM_ALLOWED_USERS", "GATEWAY_ALLOWED_USERS",
"GATEWAY_ALLOW_ALL_USERS", "TELEGRAM_GROUP_ALLOWED_USERS",
"TELEGRAM_GROUP_ALLOWED_CHATS"):
monkeypatch.delenv(var, raising=False)
(home / ".env").write_text(f"TELEGRAM_BOT_TOKEN={_VALID_BOT_TOKEN}\n", encoding="utf-8")
(home / "config.yaml").write_text("platforms:\n telegram:\n enabled: true\n", encoding="utf-8")
_gateway_state(home)
c = TestClient(app)
c.headers[_SESSION_HEADER_NAME] = _SESSION_TOKEN
return c
def _test(client, **params):
resp = client.post("/api/messaging/platforms/telegram/test", params=params)
assert resp.status_code == 200, resp.text
return resp.json()
def test_connected_deny_all_reports_posture(client):
"""Connected, but nothing a user can do would reach the agent — the exact
incident from the report (#66030): green "connected" for three days."""
payload = _test(client)
assert payload["ok"] is True
assert payload["state"] == "connected"
assert payload["auth_posture"] == "deny_all"
assert "TELEGRAM_ALLOWED_USERS" in payload["message"]
def test_connected_with_allowlist_reports_allowlist(client, monkeypatch):
from hermes_constants import get_hermes_home
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "42, 43")
payload = _test(client)
assert payload["ok"] is True
assert payload["auth_posture"] == "allowlist"
assert "2 users allowlisted via TELEGRAM_ALLOWED_USERS" in payload["message"]
def test_connected_with_global_allow_all_reports_open(client, monkeypatch):
monkeypatch.setenv("GATEWAY_ALLOW_ALL_USERS", "true")
payload = _test(client)
assert payload["ok"] is True
assert payload["auth_posture"] == "open"
def test_connected_with_gateway_allowed_users_reports_allowlist(client, monkeypatch):
monkeypatch.setenv("GATEWAY_ALLOWED_USERS", "42")
payload = _test(client)
assert payload["ok"] is True
assert payload["auth_posture"] == "allowlist"
assert "GATEWAY_ALLOWED_USERS" in payload["message"]
def test_connected_with_pairing_grant_counts_it(client):
"""An approved pairing request is a first-class grant (authz union): a store
with one approved user is reachable, not deny-all."""
from gateway.pairing import PairingStore
store = PairingStore()
store._approve_user("telegram", "42", "operator")
try:
payload = _test(client)
assert payload["auth_posture"] == "pairing"
assert "1 user paired" in payload["message"]
finally:
store.revoke("telegram", "42")
def test_connected_deny_all_mentions_pending_pairing_requests(client):
import time as _time
from gateway.pairing import PairingStore
store = PairingStore()
with store._lock:
# A pending (unapproved) request is NOT a grant; the posture stays deny_all
# and the message points the operator at the waiting request (fresh enough
# to survive the store's TTL cleanup).
pending = {"r1": {"user_id": "42", "user_name": "operator", "created_at": _time.time(),
"hash": "0" * 64, "salt": "00" * 16}}
store._save_json(store._pending_path("telegram"), pending)
try:
payload = _test(client)
assert payload["auth_posture"] == "deny_all"
assert "1 pairing request pending approval" in payload["message"]
finally:
store.clear_pending("telegram")
def test_profile_scoped_posture_ignores_root_env_and_reports_profile_grants(
client, monkeypatch, tmp_path
):
"""The dashboard process env belongs to the ROOT install: a named profile's
Test must neither borrow the root's allowlist nor miss the profile's own.
Regression for the false deny-all/false green in the #66055 review."""
import gateway.status as _gw_status
from hermes_cli import profiles as profiles_mod
from hermes_constants import get_hermes_home
default_home = get_hermes_home()
profiles_root = default_home / "profiles"
worker_home = profiles_root / "worker"
worker_home.mkdir(parents=True)
(worker_home / "config.yaml").write_text("platforms:\n telegram:\n enabled: true\n", encoding="utf-8")
(worker_home / ".env").write_text(f"TELEGRAM_BOT_TOKEN={_VALID_BOT_TOKEN}\n", encoding="utf-8")
_gateway_state(worker_home)
monkeypatch.setattr(profiles_mod, "_get_default_hermes_home", lambda: default_home)
monkeypatch.setattr(profiles_mod, "_get_profiles_root", lambda: profiles_root)
# ROOT-env allowlist must not authorize the worker profile's bot.
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111,222")
# The verified gateway identity must belong to the WORKER home: a bare "gateway
# run" belongs to the default profile, so the worker's liveness probe needs the
# ``-p worker`` command line (same identity check ``hermes -p worker status`` runs).
monkeypatch.setattr(
_gw_status, "_read_process_cmdline", lambda pid: "hermes gateway run -p worker"
)
payload = _test(client, profile="worker")
assert payload["auth_posture"] == "deny_all"
# The profile's own allowlist wins when present.
(worker_home / ".env").write_text(
f"TELEGRAM_BOT_TOKEN={_VALID_BOT_TOKEN}\nTELEGRAM_ALLOWED_USERS=7\n", encoding="utf-8"
)
payload = _test(client, profile="worker")
assert payload["auth_posture"] == "allowlist"
assert "TELEGRAM_ALLOWED_USERS" in payload["message"]