fix(secrets): route authz gate and pairing allowlist reads through the profile secret scope

- gateway/authz_mixin.py: group chat allowlists, {PLATFORM}_ALLOW_BOTS, and
  pairing-mode allowlist presence checks now go through the file's own
  _platform_gate_env helper (scoped-authoritative under multiplex).
- gateway/pairing.py: grant-mirror/revoke allowlist READS go through
  get_secret (Slack pattern for unscoped admin/CLI callers); writes still
  use save_env_value with a TODO for profile-aware writes.
This commit is contained in:
Teknium
2026-08-02 00:49:19 -07:00
parent e4306ac5e2
commit dbbfcff56d
2 changed files with 33 additions and 7 deletions
+5 -5
View File
@@ -456,7 +456,7 @@ class GatewayAuthorizationMixin:
Platform.QQBOT: "QQ_GROUP_ALLOWED_USERS",
}.get(source.platform, "")
if chat_allowlist_env:
raw_chat_allowlist = os.getenv(chat_allowlist_env, "").strip()
raw_chat_allowlist = _platform_gate_env(chat_allowlist_env)
if raw_chat_allowlist:
allowed_group_ids = {
cid.strip()
@@ -498,7 +498,7 @@ class GatewayAuthorizationMixin:
}
if getattr(source, "is_bot", False):
allow_bots_var = platform_allow_bots_map.get(source.platform)
if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in {"mentions", "all"}:
if allow_bots_var and _platform_gate_env(allow_bots_var, "none").lower().strip() in {"mentions", "all"}:
return True
if not user_id:
@@ -876,13 +876,13 @@ class GatewayAuthorizationMixin:
),
Platform.QQBOT: ("QQ_GROUP_ALLOWED_USERS",),
}
if os.getenv(platform_env_map.get(platform, ""), "").strip():
if _platform_gate_env(platform_env_map.get(platform, "")).strip():
return "ignore"
for env_key in platform_group_env_map.get(platform, ()):
if os.getenv(env_key, "").strip():
if _platform_gate_env(env_key).strip():
return "ignore"
if os.getenv("GATEWAY_ALLOWED_USERS", "").strip():
if _platform_gate_env("GATEWAY_ALLOWED_USERS").strip():
return "ignore"
return "pair"
+28 -2
View File
@@ -146,6 +146,32 @@ def _user_ids_match(platform: str, left: str, right: str) -> bool:
return bool(left_aliases and right_aliases and (left_aliases & right_aliases))
def _read_allowlist_env(env_var: str) -> str:
"""Read a platform allowlist env var through the profile secret scope.
Under multiplexing the process env may hold ANOTHER profile's allowlist
(first-writer-wins YAML→env bridges), so reads must honor the installed
scope's verdict — including a scoped miss returning empty rather than
borrowing the process value. Unscoped callers (single-profile CLI /
admin endpoints) keep the legacy ``os.getenv`` read.
TODO(profile-secrets): the grant mirror below still WRITES through
``hermes_cli.config.save_env_value`` / ``remove_env_value``, which target
the root ``.env`` — those writes need a profile-aware counterpart before
pairing grants can be mirrored correctly under multiplexing.
"""
try:
from agent.secret_scope import UnscopedSecretError, get_secret
try:
return (get_secret(env_var) or "").strip()
except UnscopedSecretError:
pass
except Exception:
pass
return (os.getenv(env_var) or "").strip()
def _sync_allowlist_add(platform: str, user_id: str) -> None:
"""Add ``user_id`` to the platform allowlist env var IF one is configured.
@@ -158,7 +184,7 @@ def _sync_allowlist_add(platform: str, user_id: str) -> None:
env_var = _allowlist_env_for_platform(platform)
if not env_var:
return
current = os.getenv(env_var, "").strip()
current = _read_allowlist_env(env_var)
if not current:
return # No allowlist configured — leave the gateway open (option i).
ids = _split_allowlist(current)
@@ -278,7 +304,7 @@ def _sync_allowlist_remove(platform: str, user_id: str) -> None:
env_var = _allowlist_env_for_platform(platform)
if not env_var:
return
current = os.getenv(env_var, "").strip()
current = _read_allowlist_env(env_var)
if not current:
return # No allowlist configured — do not touch config-only snapshots.
ids = _split_allowlist(current)