Files
YPMS/docs/coverage/registrant.md
T
Chris TonjesandClaude Opus 5.5 98b783b6b5 Phase 0 milestone 0.6: 501-stub routers for remaining entities
26 stub entities (every matrix entity except Registrant, User, Role,
AuditLog) get a router from framework/stub_router.py: one route per
matrix action (List, Search, Create, View, Edit, Delete, Restore,
VersionHistory). Each route runs the real RBAC check, writes an audit
row, then returns 501. Source RFP C.5.7.27.

- Registry in src/ypms/stubs.py; test fails if a matrix entity is
  neither real nor stubbed.
- tests/test_stubs.py sweeps entity x role x route against the matrix
  JSON: 501 + exactly one audit row when granted, 403 + none when not.
  FERPA/CFSA stubs pinned closed to non-examiner roles.
- Registrant restore now gated on the matrix's Restore action (was
  Edit, the vendored-factory convention); only the super-role holds it.
- Test fixtures (roles, make_user, api) lifted to conftest; one app per
  session (route construction dominated runtime) and a shared password
  hash for minted-token users.

138 tests passing, ruff + mypy clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:54:12 -04:00

56 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` | `Restore` | 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` and restore requires `Restore` (only the super-role holds either today) | `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+ |