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>
56 lines
5.6 KiB
Markdown
56 lines
5.6 KiB
Markdown
# 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+ |
|