# 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+ |