Files
2026-07-13 13:12:33 +08:00

334 lines
12 KiB
Python

"""Redaction round-trip + D19 secret-inheritance regression tests.
Pins the behavior of the consolidated secret-inheritance service
(``opensquilla.gateway.config_secrets``) and proves that every config-mutation
surface routes through it identically:
* the redaction round-trip restores a redaction *marker* back to the stored
secret without ever exposing the value;
* the D19 inherit-then-clear-explicit rule keeps an inherited secret marked
and unmarks a path that received an explicit new value;
* no secret value ever reaches a wire response, a persisted config file, or a
structlog event.
A single distinctive sentinel secret is used throughout so any leak is
unambiguous. These are offline, credential-free tests.
"""
from __future__ import annotations
import json
import os
import tomllib
from pathlib import Path
import pytest
import structlog
import opensquilla.gateway.rpc_config # noqa: F401 ensures config.* handlers register
from opensquilla.gateway import config_secrets
from opensquilla.gateway.auth import Principal
from opensquilla.gateway.config import GatewayConfig
from opensquilla.gateway.rpc import RpcContext, get_dispatcher
from opensquilla.gateway.rpc_config import _persist_config
from opensquilla.onboarding.mutations import upsert_llm_provider
from opensquilla.onboarding.redaction import REDACTED_PLACEHOLDER
# A secret that must never appear in any wire response, persisted file, or log.
SENTINEL_SECRET = "sk-SEN71NEL-do-not-leak-Zz9"
MARKER = config_secrets.REDACTED_PUBLIC_VALUE
def _admin_ctx(config: GatewayConfig) -> RpcContext:
return RpcContext(
conn_id="t",
config=config,
principal=Principal(
role="operator",
scopes=frozenset({"operator.admin"}),
is_owner=True,
authenticated=True,
),
)
# --- Part 1: the consolidated service in isolation (the D19 rule) -----------
def test_marker_constant_matches_read_side_redaction() -> None:
# The write-side marker must equal the read-side redaction value so a
# public config round-trips.
from opensquilla.gateway.config import _REDACTED
assert config_secrets.REDACTED_PUBLIC_VALUE == _REDACTED
def test_restore_redacted_values_inherits_marker_at_secret_path() -> None:
payload = {"remote": {"api_key": MARKER, "model": "new-model"}}
source = {"remote": {"api_key": SENTINEL_SECRET, "model": "old-model"}}
restored, redacted_paths = config_secrets.restore_redacted_values(payload, source)
# Marker inherited the stored secret; explicit non-secret value replaced.
assert restored["remote"]["api_key"] == SENTINEL_SECRET
assert restored["remote"]["model"] == "new-model"
assert redacted_paths == {"remote.api_key"}
def test_restore_redacted_values_explicit_new_secret_passes_through() -> None:
payload = {"remote": {"api_key": "sk-brand-new"}}
source = {"remote": {"api_key": SENTINEL_SECRET}}
restored, redacted_paths = config_secrets.restore_redacted_values(payload, source)
assert restored["remote"]["api_key"] == "sk-brand-new"
assert redacted_paths == set()
def test_restore_redacted_values_literal_marker_at_nonsecret_path_is_kept() -> None:
# "[redacted]" at a non-secret path is a legitimate value, not a marker.
payload = {"remote": {"model": MARKER}}
source = {"remote": {"model": "old"}}
restored, redacted_paths = config_secrets.restore_redacted_values(payload, source)
assert restored["remote"]["model"] == MARKER
assert redacted_paths == set()
def test_restore_redacted_values_marker_without_stored_secret_raises() -> None:
with pytest.raises(ValueError, match="no existing secret"):
config_secrets.restore_redacted_values(
{"remote": {"api_key": MARKER}}, {"remote": {}}
)
def test_inherit_then_clear_explicit_applies_d19_rule() -> None:
old = GatewayConfig()
old.mark_runtime_secret("llm.api_key")
old.mark_runtime_secret("search_api_key")
new = GatewayConfig()
# The path that received an explicit new value is cleared; the inherited
# (marker-preserved) path keeps its runtime-secret marker.
config_secrets.inherit_then_clear_explicit(old, new, {"search_api_key"})
assert "llm.api_key" in new._runtime_secret_paths
assert "search_api_key" not in new._runtime_secret_paths
def test_inherit_runtime_secrets_none_source_is_noop() -> None:
target = GatewayConfig()
target.mark_runtime_secret("llm.api_key")
config_secrets.inherit_runtime_secrets(None, target)
# A None source must not wipe the target's markers.
assert "llm.api_key" in target._runtime_secret_paths
# --- Part 2: RPC surface (config.get / config.apply) round-trip -------------
@pytest.mark.asyncio
async def test_rpc_secret_reads_back_as_marker_never_value(tmp_path) -> None:
cfg = GatewayConfig(
config_path=str(tmp_path / "c.toml"),
memory={"embedding": {"remote": {"api_key": SENTINEL_SECRET}}},
)
res = await get_dispatcher().dispatch("r1", "config.get", {}, _admin_ctx(cfg))
assert res.error is None, res.error
assert res.payload["memory"]["embedding"]["remote"]["api_key"] == MARKER
assert SENTINEL_SECRET not in json.dumps(res.payload)
@pytest.mark.asyncio
async def test_rpc_marker_apply_preserves_secret_and_never_leaks(tmp_path) -> None:
config_path = tmp_path / "c.toml"
cfg = GatewayConfig(
config_path=str(config_path),
memory={"embedding": {"remote": {"api_key": SENTINEL_SECRET}}},
)
dispatcher = get_dispatcher()
get_res = await dispatcher.dispatch("r1", "config.get", {}, _admin_ctx(cfg))
assert get_res.payload["memory"]["embedding"]["remote"]["api_key"] == MARKER
apply_res = await dispatcher.dispatch(
"r2", "config.apply", {"config": get_res.payload}, _admin_ctx(cfg)
)
assert apply_res.error is None, apply_res.error
# Marker resubmit inherited the stored secret: value unchanged.
assert cfg.memory.embedding.remote.api_key == SENTINEL_SECRET
# Never echoed on the wire; never written to disk as the marker string.
assert SENTINEL_SECRET not in json.dumps(apply_res.payload)
assert MARKER not in config_path.read_text(encoding="utf-8")
@pytest.mark.asyncio
async def test_rpc_runtime_marked_secret_survives_and_is_not_persisted(tmp_path) -> None:
config_path = tmp_path / "c.toml"
cfg = GatewayConfig(
config_path=str(config_path),
llm={"provider": "openai", "model": "gpt-x", "api_key": SENTINEL_SECRET},
)
# Emulate an env-sourced runtime-secret marker (as config_store stamps).
cfg.mark_runtime_secret("llm.api_key")
dispatcher = get_dispatcher()
get_res = await dispatcher.dispatch("r1", "config.get", {}, _admin_ctx(cfg))
assert get_res.payload["llm"]["api_key"] == MARKER
apply_res = await dispatcher.dispatch(
"r2", "config.apply", {"config": get_res.payload}, _admin_ctx(cfg)
)
assert apply_res.error is None, apply_res.error
# Secret preserved by value AND marker; marker keeps it off disk.
assert cfg.llm.api_key == SENTINEL_SECRET
assert "llm.api_key" in cfg._runtime_secret_paths
assert SENTINEL_SECRET not in config_path.read_text(encoding="utf-8")
assert SENTINEL_SECRET not in json.dumps(apply_res.payload)
@pytest.mark.asyncio
async def test_rpc_explicit_new_secret_replaces_and_clears_marker(tmp_path) -> None:
config_path = tmp_path / "c.toml"
cfg = GatewayConfig(
config_path=str(config_path),
memory={"embedding": {"remote": {"api_key": "sk-original"}}},
)
cfg.mark_runtime_secret("memory.embedding.remote.api_key")
res = await get_dispatcher().dispatch(
"r1",
"config.set",
{"path": "memory.embedding.remote.api_key", "value": SENTINEL_SECRET},
_admin_ctx(cfg),
)
assert res.error is None, res.error
# Explicit value replaced the stored secret and cleared its marker, so it
# is now persisted (the value belongs on disk once explicitly provided).
assert cfg.memory.embedding.remote.api_key == SENTINEL_SECRET
assert "memory.embedding.remote.api_key" not in cfg._runtime_secret_paths
persisted = tomllib.loads(config_path.read_text(encoding="utf-8"))
assert persisted["memory"]["embedding"]["remote"]["api_key"] == SENTINEL_SECRET
# The response body never carries the value.
assert SENTINEL_SECRET not in json.dumps(res.payload)
# --- Part 3: onboarding mutation surface ------------------------------------
def test_onboarding_blank_key_inherits_stored_secret() -> None:
res1 = upsert_llm_provider(
GatewayConfig(), provider_id="openrouter", model="m1", api_key=SENTINEL_SECRET
)
assert res1.config.llm.api_key == SENTINEL_SECRET
# Re-saving the same provider with a blank key keeps the stored secret.
res2 = upsert_llm_provider(
res1.config, provider_id="openrouter", model="m2", api_key=""
)
assert res2.config.llm.api_key == SENTINEL_SECRET
assert res2.public_payload["api_key"] == REDACTED_PLACEHOLDER
assert SENTINEL_SECRET not in json.dumps(res2.public_payload)
def test_onboarding_explicit_key_replaces_and_clears_marker() -> None:
res1 = upsert_llm_provider(
GatewayConfig(), provider_id="openrouter", model="m1", api_key="sk-old"
)
# Emulate a runtime-secret marker carried on the stored config.
res1.config.mark_runtime_secret("llm.api_key")
res2 = upsert_llm_provider(
res1.config, provider_id="openrouter", model="m1", api_key=SENTINEL_SECRET
)
assert res2.config.llm.api_key == SENTINEL_SECRET
assert "llm.api_key" not in res2.config._runtime_secret_paths
assert SENTINEL_SECRET not in json.dumps(res2.public_payload)
# --- Part 4: no secret value ever reaches a structlog event -----------------
# --- Part 5: env-absorbed auth token must not be baked to disk -------------
async def test_config_set_does_not_bake_env_auth_token_into_toml(
tmp_path, monkeypatch
) -> None:
monkeypatch.setenv("OPENSQUILLA_AUTH_TOKEN", "tok-env-original")
cfg_path = tmp_path / "config.toml"
cfg_path.write_text('[auth]\nmode = "token"\n')
cfg = GatewayConfig.load(cfg_path)
assert cfg.auth.token == "tok-env-original"
res = await get_dispatcher().dispatch(
"r1", "config.set", {"path": "port", "value": 18795}, _admin_ctx(cfg)
)
assert res.error is None, res.error
text = cfg_path.read_text()
assert tomllib.loads(text)["port"] == 18795
assert "tok-env-original" not in text
async def test_env_auth_token_rotation_survives_config_save(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("OPENSQUILLA_AUTH_TOKEN", "tok-env-original")
cfg_path = tmp_path / "config.toml"
cfg_path.write_text('[auth]\nmode = "token"\n')
cfg = GatewayConfig.load(cfg_path)
await get_dispatcher().dispatch(
"r1", "config.set", {"path": "port", "value": 18795}, _admin_ctx(cfg)
)
monkeypatch.setenv("OPENSQUILLA_AUTH_TOKEN", "tok-env-rotated")
rebooted = GatewayConfig.load(cfg_path)
assert rebooted.auth.token == "tok-env-rotated"
@pytest.mark.skipif(os.name == "nt", reason="Windows st_mode does not represent DACLs")
def test_persist_config_fresh_file_has_owner_only_posix_mode(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("OPENSQUILLA_AUTH_TOKEN", "tok-env-original")
old_umask = os.umask(0o022)
try:
cfg = GatewayConfig()
cfg_path = tmp_path / "fresh" / "config.toml"
cfg.config_path = str(cfg_path)
_persist_config(cfg)
finally:
os.umask(old_umask)
mode = Path(cfg_path).stat().st_mode & 0o777
assert mode & 0o077 == 0
@pytest.mark.asyncio
async def test_secret_never_appears_in_any_log_event(tmp_path) -> None:
cfg = GatewayConfig(
config_path=str(tmp_path / "c.toml"),
memory={"embedding": {"remote": {"api_key": SENTINEL_SECRET}}},
)
dispatcher = get_dispatcher()
with structlog.testing.capture_logs() as captured:
get_res = await dispatcher.dispatch("r1", "config.get", {}, _admin_ctx(cfg))
await dispatcher.dispatch(
"r2", "config.apply", {"config": get_res.payload}, _admin_ctx(cfg)
)
# Onboarding surface in the same capture window.
upsert_llm_provider(
cfg, provider_id="openrouter", model="m", api_key=SENTINEL_SECRET
)
blob = json.dumps(captured, default=repr)
assert SENTINEL_SECRET not in blob