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>
5.6 KiB
5.6 KiB
Registrant — requirement coverage
Milestone: Phase 0 / 0.5 (vertical slice) · As of: 2026-09-30
Entity: Registrant (ARCHITECTURE.md §6.1) · RBAC entity string: Registrant
Traces each requirement the Registrant slice claims to the code and test that
evidence it. Auditors: file paths are relative to ypms/.
Surface
| Method | Path | Permission | Portal-only callers | PII-read audit |
|---|---|---|---|---|
GET |
/registrants |
List |
403 | one pii_read row per page, after.ids = returned ids |
GET |
/registrants/{id} |
View |
own row only (YOUTH_USER); else 404 | pii_read row, entity_id = id |
POST |
/registrants |
Create |
403 | — (create audit row) |
PATCH |
/registrants/{id} |
Edit |
own row only (YOUTH_USER); else 404 | — (update audit row) |
DELETE |
/registrants/{id} |
Delete |
403 | — (delete audit row) |
POST |
/registrants/{id}/restore |
Edit |
403 | — (restore audit row) |
GET |
/registrants/{id}/history |
VersionHistory |
403 | pii_read row, entity_id = id |
"Portal-only" = every role the caller holds is in {YOUTH_USER, PARENT_USER, HOST_USER}. A caller with any staff role gets staff behaviour. PARENT_USER sees no rows until the Guardian link lands (Phase 1). No bulk endpoints for Registrant in Phase 0.
Requirement trace
| Requirement | What the code does | Evidence (code) | Evidence (test) |
|---|---|---|---|
Source RFP C.5.3.1, C.5.3.4 — youth application / account management |
Registrant CRUD with soft delete, restore, cursor pagination | src/ypms/youth/router.py |
tests/youth/test_registrant_api.py |
Source RFP C.5.7.50; ARCH §6.3 — SSN masked in display |
Read schema exposes only ssn_masked (XXX-XX-1234); masking in the serializer, so API consumers see it too |
src/ypms/youth/schemas.py (RegistrantRead), src/ypms/youth/models.py (ssn_masked) |
test_create_masks_ssn_and_never_returns_it |
| NIST SC-28, SC-28(1); ARCH §6.3 — app-layer encryption of SSN | AES-256-GCM, HKDF-derived key, per-value nonce, column-bound AAD, versioned format; SSN is write-only on the model | src/ypms/security/column_crypto.py, src/ypms/youth/models.py (ssn setter, decrypt_ssn) |
test_roundtrip_and_fresh_nonce, test_ciphertext_is_bound_to_column, test_ssn_is_write_only |
| NIST AU-2, AU-3, AU-12 — every state change audited | Create / update / delete / restore emit audit rows with actor, before, after | src/ypms/framework/crud.py |
test_create_emits_redacted_audit_and_version_snapshot, test_update_bumps_version_and_audits, test_delete_requires_delete_permission_then_restore |
NIST AU-14; source RFP C.5.6.9 #5 — record access to sensitive data |
GET, list, and history emit pii_read rows with actor and client IP |
src/ypms/framework/pii_audit.py, src/ypms/youth/router.py |
test_get_emits_pii_read, test_list_emits_one_pii_read_with_ids, test_history_requires_version_history_permission |
| NIST AU-9 (protection of audit info) — no secrets in the audit trail | __audit_redact__ replaces ssn_encrypted with [REDACTED] in audit before/after and version-history snapshots |
src/ypms/framework/crud.py (_to_dict), src/ypms/youth/models.py |
test_create_emits_redacted_audit_and_version_snapshot, test_update_bumps_version_and_audits |
| NIST AC-3, AC-6 — least privilege | Data-driven RBAC per route; history gated on VersionHistory (not View); delete requires Delete (OYP_STAFF lacks it) |
src/ypms/youth/router.py, src/ypms/seed/permissions_matrix.json |
test_history_requires_version_history_permission, test_delete_requires_delete_permission_then_restore, test_host_user_denied |
| NIST AC-3 — row-level restriction for external users (Phase 0 minimum) | Portal-only callers limited to own row by user_id; out-of-scope → 404 (no id probing) |
src/ypms/youth/router.py (_get_scoped, _require_staff) |
test_youth_can_read_and_edit_only_own_row, test_youth_blocked_from_staff_routes, test_parent_sees_nothing_until_guardian_link |
| NIST SI-10 — input validation | SSN format + SSA never-issued ranges; DOB not in future; ZIP, email, length limits; NOT NULL fields can't be PATCHed to null; user_id not writable |
src/ypms/youth/schemas.py |
test_create_validation, test_update_rejects_null_on_required_field, test_update_cannot_set_user_id |
| ARCH §3 "minors first" — minor status | minor_flag derived from DOB at read time, never stored |
src/ypms/youth/models.py (minor_flag, age_on) |
test_minor_flag_derived_from_dob, test_age_on_boundaries |
NIST CM-3; source RFP C.5.8.12 — schema change control |
New immutable Alembic revision | migrations/versions/0003_registrants.py |
rendered offline (alembic upgrade 0002_framework_tables:0003_registrants --sql); live Postgres run pending |
Deferred (not in 0.5)
| Item | Why deferred | Lands in |
|---|---|---|
| Banking columns (encrypted account, last-4) | Needs PAYROLL_ADMIN-only field scoping built alongside it; payroll vendor not selected (YPMS-POAM-052) |
Payroll phase |
| SSN blind index (HMAC) for full-SSN dedupe/search | ssn_last4 covers Phase 0 display/search; dedupe is an intake concern |
Phase 1 (application intake) |
Search endpoint |
Matrix grants Search; no route yet |
Phase 1 |
| Guardian-scoped access for PARENT_USER | Needs Guardian + ConsentRecord |
Phase 1 |
Account-link flow setting user_id |
Identity model open (YPMS-POAM-050) |
Phase 1 |
Key Vault–backed key + v2 ciphertext |
Phase 0 uses env-var key material | Phase 1 (YPMS-POAM-032) |
| Demographics, immigration-doc refs, tax elections | Not in PHASE0_PLAN §4 sketch | Phase 1+ |