Phase 0 milestone 0.5: Registrant vertical slice
Registrant model + schemas + router + migration 0003, exercising the vendored framework end-to-end (CRUDService, RBAC, audit, version history). - SSN app-layer encrypted (AES-256-GCM, HKDF key, column-bound AAD, versioned format) in security/column_crypto.py; write-only on the model; serializer exposes only ssn_masked (XXX-XX-1234). C.5.7.50 / SC-28. - pii_read audit on GET, list (one row per page with ids), and history. C.5.6.9 #5 / AU-14. - Portal-only callers (YOUTH/PARENT/HOST) limited to own row via Registrant.user_id; out-of-scope is 404. PARENT_USER sees nothing until Guardian lands. History gated on VersionHistory, not View. - minor_flag derived from dob at read time. Framework changes (flag to OWC on re-vendor): - crud._to_dict honors __audit_redact__, keeping ciphertext out of audit_log and version_history. - crud._to_dict serializes date (not only datetime) for JSON columns. - CRUDService.history annotation uses builtins.list (method shadowed it). 98 tests passing, 94.83% coverage; ruff + mypy clean. Coverage trace at docs/coverage/registrant.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
185b5d2af3
commit
5dd828f0e5
15 files changed
+1098
-3
No files matched your search
@@ -12,11 +12,18 @@ upstream:
|
||||
which combined with `where id > cursor` on the next call silently skipped
|
||||
that row. Caught by `tests/framework/test_crud.py::test_list_pagination_forwards_cursor`.
|
||||
Flag to OWC when re-vendoring.
|
||||
- **Snapshot redaction** (milestone 0.5): models may declare
|
||||
`__audit_redact__ = ("col", ...)`; `_to_dict` replaces those columns' values
|
||||
with `"[REDACTED]"` (nulls stay null, so "was it set" is still visible).
|
||||
Keeps app-layer ciphertext (e.g. `registrants.ssn_encrypted`) out of
|
||||
`audit_log` and `version_history`. Flag to OWC — Claimant SSN has the same
|
||||
exposure there.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import builtins
|
||||
from datetime import UTC, date, datetime
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -33,14 +40,19 @@ ModelT = TypeVar("ModelT")
|
||||
CreateT = TypeVar("CreateT", bound=BaseModel)
|
||||
UpdateT = TypeVar("UpdateT", bound=BaseModel)
|
||||
|
||||
REDACTED = "[REDACTED]"
|
||||
|
||||
|
||||
def _to_dict(obj: Any) -> dict[str, Any]:
|
||||
if obj is None:
|
||||
return {}
|
||||
redact = getattr(obj, "__audit_redact__", ())
|
||||
out: dict[str, Any] = {}
|
||||
for c in obj.__table__.columns:
|
||||
v = getattr(obj, c.name, None)
|
||||
if isinstance(v, datetime):
|
||||
if c.name in redact and v is not None:
|
||||
v = REDACTED
|
||||
elif isinstance(v, (datetime, date)):
|
||||
v = v.isoformat()
|
||||
out[c.name] = v
|
||||
return out
|
||||
@@ -157,7 +169,7 @@ class CRUDService(Generic[ModelT, CreateT, UpdateT]): # noqa: UP046 — TypeVar
|
||||
)
|
||||
db.flush()
|
||||
|
||||
def history(self, db: Session, id: int) -> list[VersionHistory]:
|
||||
def history(self, db: Session, id: int) -> builtins.list[VersionHistory]:
|
||||
stmt = (
|
||||
select(VersionHistory)
|
||||
.where(VersionHistory.entity == self.entity, VersionHistory.entity_id == id)
|
||||
|
||||
Reference in new issue
Block a user