Skip to content

Commit d4c54f4

Browse files
fix(security): fail closed when requested Fernet is unavailable (#156)
* test(security): reject unavailable Fernet before DB access * fix(security): fail closed on unavailable Fernet capability --------- Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
1 parent 032c9e8 commit d4c54f4

2 files changed

Lines changed: 53 additions & 6 deletions

File tree

pg_llm_batch/config.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,18 +300,17 @@ def __init__(self, dsn: str, fernet_key: Optional[str] = None) -> None:
300300
raise ConfigError("psycopg is required for SecretStore")
301301
if not dsn:
302302
raise ConfigError("A Postgres DSN must be provided explicitly")
303+
if fernet_key and Fernet is None:
304+
raise ConfigError(
305+
"Fernet encryption requires the optional cryptography dependency"
306+
)
303307
self.dsn = dsn
304308
self._conn = psycopg.connect(self.dsn)
305309
try:
306310
self._conn.autocommit = True
307311
self._fernet = None
308312
if fernet_key and Fernet is not None:
309313
self._fernet = Fernet(fernet_key.encode("utf-8"))
310-
elif fernet_key and Fernet is None: # pragma: no cover
311-
logger.warning(
312-
"Fernet key supplied but 'cryptography' is not installed; "
313-
"storing secrets base64-obfuscated instead."
314-
)
315314
self._ensure_table()
316315
except BaseException:
317316
self.close()
@@ -403,4 +402,4 @@ def close(self) -> None:
403402

404403
def get_config_store(dsn: str) -> PostgresConfigStore:
405404
"""Construct a config store. DSN must be passed explicitly (no getenv)."""
406-
return PostgresConfigStore(dsn)
405+
return PostgresConfigStore(dsn)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Regression tests for SecretStore encryption capability boundaries."""
3+
4+
from __future__ import annotations
5+
6+
import pytest
7+
8+
from pg_llm_batch import config as config_mod
9+
from pg_llm_batch.exceptions import ConfigError
10+
11+
12+
class _Connection:
13+
"""Minimal connection double that records deterministic cleanup."""
14+
15+
def __init__(self) -> None:
16+
self.autocommit = False
17+
self.close_calls = 0
18+
19+
def close(self) -> None:
20+
self.close_calls += 1
21+
22+
23+
class _Psycopg:
24+
"""Record whether database acquisition happened before local validation."""
25+
26+
def __init__(self) -> None:
27+
self.connect_calls = 0
28+
self.connection = _Connection()
29+
30+
def connect(self, _dsn: str) -> _Connection:
31+
self.connect_calls += 1
32+
return self.connection
33+
34+
35+
def test_fernet_request_fails_before_database_access_when_crypto_is_unavailable(
36+
monkeypatch,
37+
) -> None:
38+
"""Never downgrade an explicit encryption request to Base64 persistence."""
39+
fake_psycopg = _Psycopg()
40+
monkeypatch.setattr(config_mod, "psycopg", fake_psycopg)
41+
monkeypatch.setattr(config_mod, "Fernet", None)
42+
monkeypatch.setattr(config_mod.SecretStore, "_ensure_table", lambda _self: None)
43+
44+
with pytest.raises(ConfigError, match="Fernet"):
45+
config_mod.SecretStore("postgresql://database", fernet_key="explicit-key")
46+
47+
assert fake_psycopg.connect_calls == 0
48+
assert fake_psycopg.connection.close_calls == 0

0 commit comments

Comments
 (0)