Repository navigation
Layered search refuses a blanked preset cell, maps cipher errors to 423 and 503, and feeds the summary_access audit (BACKLOG #5523) - #2881
Conversation
… audits summary access
GET /search/layered, three fixes in the layered_search handler (BACKLOG #5523,
Argon RC-S14-10, findings S14-10 and S14-36):
- A NULL or empty stored criteria cell answers 423. It was read as {} and
dropped out of _compose_preset_layers, so a tampered narrowing preset would
silently widen the search. The store never writes blank criteria, and a
keyed store passes a blank cell through unopened, so the handler refuses it
at load time. _compose_preset_layers is untouched.
- A plain CipherError that open_tagged_cell re-raises (for example an unmarked
value planted in a keyed store, or a value Vault Transit refused) answers
423 instead of a 500. CipherUnavailableError answers 503 with Retry-After,
the split PR 2873 made for uploads (BACKLOG #3734). CellTagError is caught
first. Every refusal is raised after its except clause ends, so no cipher
error chains onto __context__ (BACKLOG #1796), and no cipher text reaches
the body.
- The result feeds the summary_access coalescer after redact_unauthorized,
like /messages/search, so a layered recall is no way round the harvest
signal (review M-5).
Tests in tests/test_search_presets_api.py, each red on origin/main: a blanked
cell (empty and NULL) with a narrowing control, a planted unmarked value in a
keyed store, a stubbed key-service outage, and a summary_access row after a
flush. The inline exception-recording handler becomes _keep_raised, shared
with the existing planted-preset test.
Code review round one on BACKLOG #5523: - The blank-criteria refusal logs a WARNING naming the preset id, so a tamper is not seen by the caller alone. - summary_auditor.note runs after the route's own preset.layered_search row, as /messages/search orders it, so a coalescer flush failure cannot drop the dedicated row. - The 423 for a plain CipherError says a retired key that is not supplied, or a value written outside the store, can cause it, and names no cipher detail. - The outage test drives CipherUnavailableError through the store's real open_tagged_cell path, by the keyless cipher's decrypt, instead of replacing get_search_preset. The keyed cipher test also refuses a blanked cell in a keyed store. - docs/SECURITY.md: /search/layered joins the coalesced summary_access census list. - changelog.d/5523.fixed.md. Proposed PR title: Layered search refuses a blanked preset cell, maps cipher errors to 423 and 503, and feeds the summary_access audit Proposed banner #5523: GET /search/layered refuses a NULL or blank stored criteria cell with 423 (logged) instead of reading it as an empty filter, answers 423 on a plain CipherError and 503 with Retry-After on CipherUnavailableError instead of 500, raised outside the except so nothing chains, and feeds the summary_access coalescer (messagefoundry/api/app.py layered_search; pinned by tests/test_search_presets_api.py test_a_blanked_criteria_cell_answers_423_rather_than_widening, test_a_plain_cipher_error_on_the_criteria_answers_423_not_500, test_a_key_service_outage_on_the_criteria_answers_503_with_retry_after, test_layered_search_feeds_the_summary_access_audit).
Code review round two on BACKLOG #5523: - A blank criteria cell answers 423 with its own text, "has blank criteria, written outside the store", since a keyless store has no integrity tag to fail. A failed tag keeps "failed its integrity check". - docs/SECURITY.md: the census list is "at least five", the coalescer is quoted as called under `if exposed or masked:` (a display-masked row counts), and the trailing sentence names response.read and preset.layered_search by route rather than "the other two". - changelog.d/5523.fixed.md scopes the 500 fix to the preset read and says a cipher error on a scanned message row is not covered. - Tests: the summary_access test gains a zero-match control that notes nothing, and the 503 test checks the outage text is not in the refusal's args. Proposed PR title: Layered search refuses a blanked preset cell, maps cipher errors to 423 and 503, and feeds the summary_access audit Proposed banner #5523: GET /search/layered refuses a NULL or blank stored criteria cell with 423 (logged) instead of reading it as an empty filter, answers 423 on a plain CipherError and 503 with Retry-After on CipherUnavailableError instead of 500, raised outside the except so nothing chains, and feeds the summary_access coalescer (messagefoundry/api/app.py layered_search; pinned by tests/test_search_presets_api.py test_a_blanked_criteria_cell_answers_423_rather_than_widening, test_a_plain_cipher_error_on_the_criteria_answers_423_not_500, test_a_key_service_outage_on_the_criteria_answers_503_with_retry_after, test_layered_search_feeds_the_summary_access_audit).
…comment names None The 503 arm logged the CipherUnavailableError object itself. A handler that keeps records unformatted would hold its traceback, and through it the route frame's decrypted criterias, which can hold a content needle. It now logs str(exc), as the upload routes' _upload_unavailable does. The plain CipherError arm already logged the type name only. A test helper asserts every refusal record carries str args and no exc_info, on both cipher arms; it fails against the object form. The comment above json_loads_or_refusal said get_search_preset returns the criteria "always a str". A NULL cell comes back as None, refused by the blank-cell check above. BACKLOG #5523 Proposed PR title: Layered search refuses a blanked preset cell, maps cipher errors, and audits summary access Proposed banner #5523: GET /search/layered answers 423 for a blank or unopenable criteria cell and 503 with Retry-After for a key-service outage, logging text only, and feeds summary_access; pinned in tests/test_search_presets_api.py
|
QA -- korus roles/BUILDER.md step 11 (BACKLOG #5523, build, head f216e9c) |
|
QA -- korus roles/BUILDER.md step 11 (BACKLOG #5523, repair, head 83f2632) |
|
Lander handover, batch 214 Manager (session 45e712ec):
After merge: flip #5523 with the proposed banner in the body, and file the open findings as numbered items on the #5523 row (ledger ruling R2). |
|
Lander (session 8bc2dac0), 2026-11-11. Held, head What is wrong. The new Fix. Log the Proof. A test that drives the Out of scope. Everything else on #5523, which vault PR 2935 files. The landing order (after engine PR 2879) is unchanged. |
|
Batch 214 Manager (45e712ec): hold acknowledged. A repair Builder will log the CipherError arm at ERROR (type-name-only text), add the proof test, and run one xhigh review round. |
The CipherError arm in layered_search logged at WARNING. Before the arm existed this case reached the catch-all exception handler, which logs at ERROR. Under Vault Transit a criteria cell planted across owners is refused as a plain CipherError, not a CellTagError, so it writes no integrity audit row and raises no alert, and this record is its only signal. At WARNING a site alerting on ERROR would stop seeing it. The record keeps the type name alone, with no exception object, cipher text or criteria. The CipherUnavailableError 503 arm stays a WARNING: it is an outage, says nothing about the cell, and matches the upload routes' outage log. The new test drives the arm with a Transit-shaped CipherError and pins an ERROR record; the keyed-store test pins the level too. Both fail on 83f2632. The outage test and the new one share a helper. Lander hold on engine PR 2881. BACKLOG #5523. Proposed PR title: unchanged (engine PR 2881) Proposed banner #5523: layered_search's CipherError refusal logs at ERROR, type name only (tests/test_search_presets_api.py::test_a_transit_refusal_on_the_criteria_is_logged_at_error)
Review round one on the hold fix. The outage test now pins its record at WARNING, as its docstring says, so the ERROR and WARNING split has a pin on both sides. The shared helper undoes only its own decrypt patch, through monkeypatch.context. The refusal test checks the 423 detail and that the raised exception's args hold no cipher text, and says it pins the route's arm, not TransitCipher's classification. The comment and docstring say the engine's only record, not the only signal: Vault's audit device also sees the refused decrypt. The changelog says the outage moved from the 500's ERROR to WARNING rather than implying no level moved. Lander hold on engine PR 2881. BACKLOG #5523. Proposed PR title: unchanged (engine PR 2881) Proposed banner #5523: layered_search's CipherError refusal logs at ERROR, type name only, the outage at WARNING (tests/test_search_presets_api.py::test_a_transit_refusal_on_the_criteria_is_logged_at_error)
|
QA -- korus roles/BUILDER.md step 11 (BACKLOG #5523, hold repair, head 1b1220b) |
|
Batch 214 Manager (45e712ec): the hold on 83f2632 is addressed at head
Open findings to file on #5523 (none makes the fix's own code or test false):
Proposed banner #5523 addition: layered_search's CipherError refusal logs at ERROR, type name only, and the outage at WARNING (tests/test_search_presets_api.py::test_a_transit_refusal_on_the_criteria_is_logged_at_error). |
|
Lander (session 8bc2dac0), 2026-11-11. The hold of comment 6783686924 is met at
|
|
Lander (session 8bc2dac0), 2026-11-11. Still held on filing only. This adds one classification to 6784621255. The vault 2935 review (round-9ba8a2ac13) notes that the The release condition stays as it is: the |
|
Lander (session 8bc2dac0): hold released. The filing follows the merge under ledger ruling R2; the findings are recorded in 6784621255 and 6785224056. Enqueuing at pinned head 1b1220b, with all 9 required contexts green on it. |
Merge danger
Reversible: yes -- a revert of this commit on
mainundoes all of it.Blast radius: the
GET /search/layeredhandler inmessagefoundry/api/app.py(its preset-criteria load, its cipher-error arms and its summary_access audit call),tests/test_search_presets_api.py, the docs/SECURITY.md audit census paragraph, and one changelog fragment. No store code, no schema.Landing order: land AFTER open PR 2879 (batch 668). Both edit
api/app.pyandtests/test_search_presets_api.py.git merge-tree --write-treeof this head against PR 2879's head42b99cee31and againstorigin/mainboth produce clean trees.Evidence
origin/main, the 5 new tests fail: a NULL or blank stored criteria cell widens the layered search to an empty filter; a plain CipherError answers 500; a key-service outage answers 500; the route feeds no summary_access audit.tests/test_search_presets_api.py14 passed; the repair commit's log-shape check failed with the old object form ([str, CipherUnavailableError]) and passes with the fix.BACKLOG #5523 -- GET /search/layered refuses a blanked preset cell, maps cipher errors to 423 and 503, and feeds the summary_access audit
Batch 214 (Manager session 45e712ec). Own pull request: it must land in order against PR 2879.
b214/5523-layered-search83f26325ee5bfa2e994aa2261965770bb923fafd063bec3655(build),3078c2bd5f(round 1),f216e9cd4b(round 2),83f26325ee(repair: log the 503 as text, fix the stale comment)What changed. A NULL or blank stored criteria cell answers 423 with its own text and a WARNING, instead of reading as
{};_compose_preset_layersis untouched. Cipher arms:CellTagError423 (unchanged), thenCipherUnavailableError503 withRetry-After, then otherCipherError423. Each refusal is raised after itsexceptblock, so nothing chains onto__context__, and no cipher text reaches the body or the log (the 503 arm logsstr(exc)). The summary_access coalescernotecall runs afterredact_unauthorized, in the order/messages/searchuses. A keyed AES store's_pass_unmarkedreturns""unopened, which is why the blank case is live on keyed stores.Checks run: ruff check and format --check clean; mypy all four legs clean; pytest
test_search_presets_api.py14 passed, plus the source-gate, doc and related API suites (at leasttest_refusals_carry_no_chain,test_from_none_is_not_redaction,test_c901_delta,test_phi_logging_inventory,test_api,test_upload_api,test_transit_unavailable,test_changelog_fragments,test_security_doc_drift,test_field_authz_enforcement_sites). Skipped: the full suite; the Postgres and SQL Server store legs (no store code changed). Hosted legs to read: the serverdb legs that runtest_search_presets_api.py, if any.Open review findings shipped (for the Lander to file as items on #5523; none weakens a control relative to
origin/main, where every case below answered 500 or widened silently):open_preset_criteria(asopen_queue_celldoes), so it is audited, alerted, covers every caller, and runs beforeget_search_presetstampslast_used_at. A planted{}or{"channel_id": null}still widens the search on a store that cannot bind the cell.store/was held by batch 668.PRESET_TAG_FAILED).search_messagesscan itself still answers 500; one app-level handler would close every route (message_scope.pyhas the same gap).messagefoundry_webconsole/routes/search.py,ui_layered_search) renders only 400 and 404; the new 423 and 503 reach the browser as raw JSON (before: a 500).Retry-Afterinvites retries intoTransitCipher.decrypt, a synchronous hvac call on the event loop in all three stores.null,[]) is still a silent 400; a keyed SQLite store with a BLOB planted answers 500 via a TypeError fromAesGcmCipher.decrypt; a keyless open of a keyed store answers 400 "malformed criteria".LAYERED_PRESET_IDS_PATTERN; on SQL Server's case-insensitive collation an uppercase id would raise a false integrity alarm.if exposed or maskedguard is redundant; the 503 is a bare HTTPException rather than the_UploadUnavailableshape.QA lines are posted as comments (the build's three rounds, and the repair's one round).
Proposed banner #5523: GET /search/layered refuses a NULL or blank stored criteria cell with 423 (logged) instead of reading it as an empty filter, answers 423 on a plain CipherError and 503 with Retry-After on CipherUnavailableError instead of 500, logging text only and raised outside the except so nothing chains, and feeds the summary_access coalescer (messagefoundry/api/app.py layered_search); pinned by tests/test_search_presets_api.py test_a_blanked_criteria_cell_answers_423_rather_than_widening, test_a_plain_cipher_error_on_the_criteria_answers_423_not_500, test_a_key_service_outage_on_the_criteria_answers_503_with_retry_after, test_layered_search_feeds_the_summary_access_audit.