← 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