← back to Unclaimed Property Platform
services/common/public_projection.py
39 lines
"""Single source of truth for what an ANONYMOUS search response may contain.
Both search paths (services/search/search_api.py and SqliteRepository.masked_search) must
return exactly this field set and nothing else. `assert_public_safe` fails CLOSED — a
projection that leaks a restricted field raises instead of shipping. This turns the
public/restricted boundary from a convention (a hand-maintained SELECT list) into an
enforced invariant covered by tests. (Security audit findings 1.4/1.5/1.6.)
"""
from __future__ import annotations
PUBLIC_SEARCH_FIELDS = frozenset({
"public_reference",
"jurisdiction",
"owner_name_masked",
"owner_city",
"holder_name",
"property_type",
"amount_band",
})
# Substrings that must never appear in a public projection key. Catches the exact
# divergence the audit found (masked_search returning `holder_name_raw`) and blocks the
# obvious future leaks (ssn/dob/full address/exact amount/postal/claim evidence/normalized).
_FORBIDDEN_SUBSTRINGS = (
"ssn", "dob", "birth", "raw", "address", "amount_exact",
"postal", "evidence", "_normalized", "tax",
)
def assert_public_safe(row: dict) -> dict:
"""Raise if `row` contains any non-allowlisted or restricted-looking field."""
extra = set(row) - PUBLIC_SEARCH_FIELDS
if extra:
raise ValueError(f"projection leaked non-public fields: {sorted(extra)}")
bad = [k for k in row if any(s in k.lower() for s in _FORBIDDEN_SUBSTRINGS)]
if bad:
raise ValueError(f"projection field name looks restricted: {bad}")
return row