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
188 lines
7.9 KiB
Python
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"]
|