"""Regression: GET /settings/api/bulk must not leak nested plaintext secrets. Two intertwined defects, fixed together: - ``DataSanitizer.redact_value`` did not recurse into subtree dicts, so a namespace request (``keys[]=llm`` -> ``get_setting("llm")`` returns ``{"openai.api_key": "sk-...", ...}``) shipped nested secrets in the clear, because the outer key ``llm`` is not itself sensitive. - ``SettingsManager.__query_settings`` interpolated the caller-supplied key into a ``LIKE`` pattern unescaped, so ``keys[]=%`` matched every dotted key and dumped the whole settings table. These use a real in-memory ``SettingsManager`` so the actual LIKE query and subtree assembly run; each fails if its fix is reverted. """ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from local_deep_research.database.models import Base from local_deep_research.database.models.settings import Setting from local_deep_research.security.data_sanitizer import DataSanitizer from local_deep_research.settings.manager import SettingsManager _SECRET = "sk-REALSECRET12345" def _manager(rows): engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) session = sessionmaker(bind=engine)() for key, value in rows: session.add(Setting(key=key, value=value, type="app", name=key)) session.commit() return SettingsManager(db_session=session) def _bulk_value(manager, key): """Mirror what GET /settings/api/bulk ships for one requested key.""" return DataSanitizer.redact_value(key, None, manager.get_setting(key)) def test_namespace_request_does_not_leak_nested_secret(): """keys[]=llm must redact nested *.api_key, not ship them plaintext.""" manager = _manager( [ ("llm.provider", "openai"), ("llm.openai.api_key", _SECRET), ] ) shipped = _bulk_value(manager, "llm") assert _SECRET not in str(shipped) assert shipped["openai.api_key"] == DataSanitizer.REDACTION_TEXT # non-secret siblings stay readable assert shipped["provider"] == "openai" def test_wildcard_key_does_not_dump_settings_table(): """keys[]=% must not wildcard-match every dotted setting.""" manager = _manager( [ ("llm.provider", "openai"), ("llm.openai.api_key", _SECRET), ("search.max_results", "10"), ] ) # With the bug, get_setting("%") returns a populated dump of the table. assert not manager.get_setting("%") def test_underscore_in_key_matched_literally(): """A requested key with '_' must not act as a single-char wildcard.""" manager = _manager( [ ("a_b.foo", "t1"), ("a_b.baz", "t2"), ("axb.bar", "decoy"), ] ) # subtree request 'a_b' must not also pull 'axb.*' via the '_' wildcard result = manager.get_setting("a_b") assert result == {"foo": "t1", "baz": "t2"} assert "bar" not in result def test_exact_secret_request_still_redacted(): """Regression: an exact keys[]=llm.openai.api_key stays redacted.""" manager = _manager([("llm.openai.api_key", _SECRET)]) assert ( _bulk_value(manager, "llm.openai.api_key") == DataSanitizer.REDACTION_TEXT ) def test_json_list_of_dicts_leaf_does_not_leak_secret(): """A JSON list-of-dicts setting must not ship nested secrets via bulk. A ``ui_element="json"`` setting round-trips as an actual list, so redaction has to recurse into list items, not just dicts. """ engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) session = sessionmaker(bind=engine)() session.add( Setting( key="mcp.servers", value=[{"name": "prod", "api_key": _SECRET}], type="app", name="mcp.servers", ui_element="json", ) ) session.commit() manager = SettingsManager(db_session=session) shipped = _bulk_value(manager, "mcp.servers") assert _SECRET not in str(shipped) assert shipped[0]["api_key"] == DataSanitizer.REDACTION_TEXT assert shipped[0]["name"] == "prod"