Compare commits
1 commit
main
...
feat/secre
| Author | SHA1 | Date | |
|---|---|---|---|
| 14a59708ee |
4 changed files with 155 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -82,3 +82,4 @@ logs/
|
||||||
.cache/
|
.cache/
|
||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
|
validate_live_report.json
|
||||||
|
|
|
||||||
34
settings.py
34
settings.py
|
|
@ -184,9 +184,41 @@ class PrySettings(BaseSettings):
|
||||||
rate_limit_per_ip: int = 60
|
rate_limit_per_ip: int = 60
|
||||||
|
|
||||||
def model_post_init(self, __context: Any) -> None:
|
def model_post_init(self, __context: Any) -> None:
|
||||||
"""Load runtime overrides from the JSON config file after env parsing."""
|
"""Post-init: pull sensitive fields from secrets_backend, then config file."""
|
||||||
|
self._apply_secrets_backend()
|
||||||
self._load_config_file()
|
self._load_config_file()
|
||||||
|
|
||||||
|
def _apply_secrets_backend(self) -> None:
|
||||||
|
"""Override sensitive fields with values from secrets_backend if present.
|
||||||
|
|
||||||
|
Priority per field:
|
||||||
|
1. secrets_backend.get_secret(name) # gopass/env/file/auto
|
||||||
|
2. PRY_<NAME> env var (handled by BaseSettings)
|
||||||
|
3. field default
|
||||||
|
|
||||||
|
Sensitive fields: api_key, *_api_key, webhook_secret, jwt_secret,
|
||||||
|
database_url, x402_wallet, x402_facilitator. We only override if
|
||||||
|
the field is currently empty (i.e. nothing was provided via env or
|
||||||
|
the field default is empty).
|
||||||
|
"""
|
||||||
|
sensitive = (
|
||||||
|
"api_key", "openrouter_api_key", "openai_api_key",
|
||||||
|
"anthropic_api_key", "cohere_api_key", "webhook_secret",
|
||||||
|
# x402_wallet/x402_facilitator live in x402.py; jwt_secret/database_url have their own wiring,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
from secrets_backend import get_secret
|
||||||
|
except ImportError:
|
||||||
|
return
|
||||||
|
for fname in sensitive:
|
||||||
|
current = getattr(self, fname, None)
|
||||||
|
if current:
|
||||||
|
# Already populated from env or default — respect it
|
||||||
|
continue
|
||||||
|
value = get_secret(fname)
|
||||||
|
if value:
|
||||||
|
setattr(self, fname, value)
|
||||||
|
|
||||||
def _load_config_file(self) -> None:
|
def _load_config_file(self) -> None:
|
||||||
path = self.config_file
|
path = self.config_file
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
|
|
||||||
|
|
@ -112,3 +112,38 @@ def _make_lenient_logrecord() -> None:
|
||||||
|
|
||||||
# Install the lenient LogRecord at conftest import time
|
# Install the lenient LogRecord at conftest import time
|
||||||
_make_lenient_logrecord()
|
_make_lenient_logrecord()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test isolation: disable gopass + clear auth in settings ─────
|
||||||
|
# PrySettings now pulls api_key from secrets_backend (gopass on Talos has
|
||||||
|
# a real pry/api_key secret). Tests must not depend on that real secret.
|
||||||
|
# Solution: set PRY_SECRET_BACKEND=env at session scope (inherited by
|
||||||
|
# subprocess-based tests like the SSE server test) and zero the
|
||||||
|
# settings singleton at test scope.
|
||||||
|
@pytest.fixture(autouse=True, scope="session")
|
||||||
|
def _pry_disable_secrets_gopass() -> None:
|
||||||
|
import os
|
||||||
|
os.environ["PRY_SECRET_BACKEND"] = "env"
|
||||||
|
os.environ.setdefault("PRY_API_KEY", "")
|
||||||
|
os.environ.setdefault("PRY_OPENROUTER_API_KEY", "")
|
||||||
|
os.environ.setdefault("PRY_OPENAI_API_KEY", "")
|
||||||
|
os.environ.setdefault("PRY_ANTHROPIC_API_KEY", "")
|
||||||
|
os.environ.setdefault("PRY_COHERE_API_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def pry_test_auth(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
import settings as _settings_mod
|
||||||
|
original = _settings_mod.settings.api_key
|
||||||
|
_settings_mod.settings.api_key = ""
|
||||||
|
yield
|
||||||
|
_settings_mod.settings.api_key = original
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> "fastapi.testclient.TestClient": # noqa: F821
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from api import app
|
||||||
|
|
||||||
|
return TestClient(app)
|
||||||
|
|
|
||||||
86
tests/test_secrets_backend_integration.py
Normal file
86
tests/test_secrets_backend_integration.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Rug Munch Media LLC
|
||||||
|
#
|
||||||
|
# Part of Pry - https://git.rugmunch.io/RugMunchMedia/pryscraper
|
||||||
|
# Licensed under MIT. See LICENSE.
|
||||||
|
"""Verify x402 and settings use secrets_backend.get_secret() for sensitive values."""
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_pulls_from_secrets_backend() -> None:
|
||||||
|
"""PrySettings should populate sensitive fields from secrets_backend.get_secret
|
||||||
|
when env vars are not set."""
|
||||||
|
from pydantic import PydanticUserError
|
||||||
|
|
||||||
|
# Force a known value from secrets_backend
|
||||||
|
fake_secret = "fake-secret-from-gopass"
|
||||||
|
|
||||||
|
def fake_get_secret(name: str, default: str = "") -> str:
|
||||||
|
if name == "openrouter_api_key":
|
||||||
|
return fake_secret
|
||||||
|
return default
|
||||||
|
|
||||||
|
# Clear env so the test is deterministic
|
||||||
|
env = {k: v for k, v in os.environ.items() if not k.startswith("PRY_")}
|
||||||
|
env["PRY_OPENROUTER_API_KEY"] = "" # explicitly empty
|
||||||
|
|
||||||
|
with patch.dict(os.environ, env, clear=True):
|
||||||
|
with patch("secrets_backend.get_secret", side_effect=fake_get_secret):
|
||||||
|
# Re-import fresh
|
||||||
|
import importlib
|
||||||
|
from settings import PrySettings
|
||||||
|
|
||||||
|
s = PrySettings()
|
||||||
|
# Sensitive field was empty in env, secrets_backend provided it
|
||||||
|
assert s.openrouter_api_key == fake_secret, (
|
||||||
|
f"Expected {fake_secret!r}, got {s.openrouter_api_key!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_x402_constants_come_from_secrets_backend() -> None:
|
||||||
|
"""x402.py X402_WALLET and X402_FACILITATOR_URL should be settable
|
||||||
|
via secrets_backend.get_secret()."""
|
||||||
|
fake_wallet = "0xABCDEF1234567890"
|
||||||
|
fake_facilitator = "https://my-facilitator.example.com"
|
||||||
|
|
||||||
|
def fake_get_secret(name: str, default: str = "") -> str:
|
||||||
|
if name == "x402_wallet":
|
||||||
|
return fake_wallet
|
||||||
|
if name == "x402_facilitator":
|
||||||
|
return fake_facilitator
|
||||||
|
return default
|
||||||
|
|
||||||
|
env = {k: v for k, v in os.environ.items() if not k.startswith("PRY_X402_")}
|
||||||
|
|
||||||
|
with patch.dict(os.environ, env, clear=True):
|
||||||
|
with patch("secrets_backend.get_secret", side_effect=fake_get_secret):
|
||||||
|
# Force re-import of x402 constants
|
||||||
|
import importlib
|
||||||
|
import x402
|
||||||
|
|
||||||
|
importlib.reload(x402)
|
||||||
|
assert x402.X402_WALLET == fake_wallet
|
||||||
|
assert x402.X402_FACILITATOR_URL == fake_facilitator
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_does_not_override_set_env() -> None:
|
||||||
|
"""If the env var is already set, secrets_backend must NOT override it."""
|
||||||
|
fake_secret = "from-gopass"
|
||||||
|
from_env = "from-env"
|
||||||
|
|
||||||
|
def fake_get_secret(name: str, default: str = "") -> str:
|
||||||
|
if name == "openrouter_api_key":
|
||||||
|
return fake_secret
|
||||||
|
return default
|
||||||
|
|
||||||
|
env = {k: v for k, v in os.environ.items() if k != "PRY_OPENROUTER_API_KEY"}
|
||||||
|
env["PRY_OPENROUTER_API_KEY"] = from_env
|
||||||
|
|
||||||
|
with patch.dict(os.environ, env, clear=True):
|
||||||
|
with patch("secrets_backend.get_secret", side_effect=fake_get_secret):
|
||||||
|
from settings import PrySettings
|
||||||
|
|
||||||
|
s = PrySettings()
|
||||||
|
# Env wins over secrets_backend
|
||||||
|
assert s.openrouter_api_key == from_env
|
||||||
Loading…
Add table
Add a link
Reference in a new issue