diff --git a/PHASE0_PLAN.md b/PHASE0_PLAN.md index 6f0ef50..826fc74 100644 --- a/PHASE0_PLAN.md +++ b/PHASE0_PLAN.md @@ -75,7 +75,7 @@ Every Phase 0 component maps to a source-RFP requirement or a NIST control. Tabl | **0.2** | Framework vendor-in | ✅ **Complete 2026-09-30.** OWC framework (base, bulk, crud, pagination, router, version_history) + security (models, rbac) + audit (models, service) + users (models) + auth stub vendored with owc→ypms renames. YPMS-specific `framework/pii_audit.py` added on top. Pagination bug in upstream fixed here (peek-ahead cursor was silently dropping rows). 26 tests passing, 93.10% coverage. Migration `0002_framework_tables.py` creates the shared tables. | | **0.3** | Auth + JWT + assurance_level | ✅ **Complete 2026-09-30.** `POST /auth/login` (OAuth2 form) returns access + refresh JWTs; access token carries `sub`, `uid`, `roles`, `iam.assurance_level`, `mfa_verified`, `iat`, `exp`, `type`. `get_current_user` verifies and rejects refresh tokens. `/auth/refresh`, `/auth/me`, `/auth/change-password` (≥12 char floor) also shipped. Login lockout after 5 IP-scoped failed attempts / 15 min per NIST AC-7. 43 tests, 93.47% coverage. | | **0.4** | RBAC seed loaded | ✅ **Complete 2026-09-30.** 8 roles seeded from `seed/roles.py`. Permissions matrix at `seed/permissions_matrix.json` — role-first grants across 30 entities × 8 actions. `python -m ypms.seed.run` wired into docker-compose. Loader validates every cell against declared actions/entities and refuses unknown roles at seed time. 53 tests, 92.20% coverage; end-to-end `can()` decisions verified per role including admin bypass. | -| **0.5** | Registrant vertical slice | `Registrant` model + schemas + router + migration + permissions + tests. CRUD works end-to-end; SSN masked in serializer output; `@audit_read("pii_access")` fires on GET; every state change emits audit row. Coverage doc at `docs/coverage/registrant.md`. | +| **0.5** | Registrant vertical slice | ✅ **Complete 2026-09-30.** `Registrant` model + schemas + router (`src/ypms/youth/`) + migration `0003_registrants`. SSN AES-256-GCM encrypted at the app layer (`security/column_crypto.py`), write-only on the model, serializer emits only `XXX-XX-1234`. `pii_read` audit on GET, list, and history; every state change emits an audit row with ciphertext redacted via new `__audit_redact__` framework hook. Portal-only roles restricted to own row (Phase 0 minimum; PARENT_USER blocked until Guardian). 98 tests, 94.83% coverage. Coverage doc at `docs/coverage/registrant.md`. | | **0.6** | 501-stub routers for remaining entities | Every entity in ARCH §6.1 (except Registrant, User, Role, Permission, AuditRow) has a router registered in `main.py` that returns 501 while still firing RBAC + audit. Test asserts RBAC decision and audit emission for each. | | **0.7** | Retention scaffold | `retention_policy` attribute on every entity model (even 501-stub entities have a model). Nightly job in `src/ypms/framework/retention.py` runs, logs "0 records to purge" — no-op but wired. | | **0.8** | CI pipeline | GitHub Actions workflow with lint (ruff), types (mypy/pyright — match OWC's pick), tests, bandit, pip-audit, gitleaks, SBOM. Green build on `main`. | diff --git a/PHASE0_STATUS.md b/PHASE0_STATUS.md index 563d51a..3cb5637 100644 --- a/PHASE0_STATUS.md +++ b/PHASE0_STATUS.md @@ -9,7 +9,7 @@ ## TL;DR -Phase 0 milestones **0.1 → 0.4** are shipped. **0.5 (Registrant vertical slice)** and **0.6 (501-stub routers for the remaining entities)** are next; **0.7–0.9** follow. All ATO artifacts (SSP, control mapping, POA&M, PIA) are drafted at v0.1–v0.3 and awaiting review signatures. The repo builds, tests are green (**53 passing, 92% coverage**), and Docker Compose boots. Not yet pushed to Gitea — auth on `gitea.doesworks.net` needs a local `git push` from the operator's terminal. +Phase 0 milestones **0.1 → 0.5** are shipped. **0.6 (501-stub routers for the remaining entities)** is next; **0.7–0.9** follow. All ATO artifacts (SSP, control mapping, POA&M, PIA) are drafted at v0.1–v0.3 and awaiting review signatures. The repo builds, tests are green (**98 passing, 94.83% coverage**), and Docker Compose boots. `main` is pushed to Gitea over SSH. --- @@ -17,12 +17,12 @@ Phase 0 milestones **0.1 → 0.4** are shipped. **0.5 (Registrant vertical slice | Dimension | Status | |---|---| -| Phase | 0 (Foundations), milestones 0.1–0.4 shipped | -| Git branch | `main`, 9 commits ahead of `origin` (Gitea `gitea.doesworks.net/chris.tonjes/YPMS.git`) | -| Tests | **53 passing**, **92.20% coverage** (floor 80% enforced in CI) | -| Lint / types | `ruff` clean, `mypy` clean (32 files, strict) | -| Runtime | FastAPI + Postgres on Docker Compose; boots to `/healthz` + `/readyz`; `/auth/*` live | -| Migrations | 2 revisions applied (`0001_baseline`, `0002_framework_tables`) | +| Phase | 0 (Foundations), milestones 0.1–0.5 shipped | +| Git branch | `main`, tracking `origin/main` (Gitea `git@gitea.doesworks.net:chris.tonjes/YPMS.git`) | +| Tests | **98 passing**, **94.83% coverage** (floor 80% enforced in CI) | +| Lint / types | `ruff` clean, `mypy` clean (37 files, strict) | +| Runtime | FastAPI + Postgres on Docker Compose; boots to `/healthz` + `/readyz`; `/auth/*` and `/registrants/*` live | +| Migrations | 3 revisions (`0001_baseline`, `0002_framework_tables`, `0003_registrants`); 0003 rendered offline, live Postgres run pending | | Seed | 8 roles + permissions matrix + bootstrap admin (`chris` / `DOES_SYSTEM_ADMIN`) | | ATO docs | SSP v0.3, control mapping v0.2, POA&M v0.1, PIA v0.1 — all drafts, awaiting signatures | @@ -79,27 +79,27 @@ Access token carries: `sub`, `uid`, `roles`, `iam.assurance_level`, `mfa_verifie Guardrails baked in: `OYP_STAFF` can't see FERPA/CFSA (that's `ELIGIBILITY_EXAMINER`); `PAYROLL_ADMIN` can't either; `REPORTING_ANALYST` is strictly read-only; external portal roles never touch admin surfaces. +### 0.5 — Registrant vertical slice ✅ + +- `src/ypms/youth/{models,schemas,router}.py` + `migrations/versions/0003_registrants.py` +- `src/ypms/security/column_crypto.py` — AES-256-GCM, HKDF-SHA256 key from `COLUMN_ENCRYPTION_KEY` (≥32 bytes enforced), per-value nonce, column name as AAD, `v1.` prefix for rotation +- SSN is **write-only** on the model (`registrant.ssn = ...` encrypts + sets `ssn_last4`); the only read path is `decrypt_ssn()`. Serializer exposes `ssn_masked` only. +- `pii_read` audit on GET, list (one row per page, ids in `after.ids`), and history — `framework/pii_audit.py` gained `emit_pii_list_read` +- Own-row scoping for portal-only callers via nullable `Registrant.user_id` (set by Phase 1 account-link flow; not API-writable). PARENT_USER sees nothing until Guardian lands. +- Hand-built router on `CRUDService` instead of `make_router` — needs PII-read audit, own-row scoping, and `VersionHistory`-gated history. No bulk endpoints for Registrant. +- **Framework changes** (flag to OWC on re-vendor): `crud._to_dict` honors `__audit_redact__` (ciphertext → `[REDACTED]` in audit + version history) and serializes `date`; `CRUDService.history` annotation uses `builtins.list` (the `list` method shadowed it for mypy). +- Evidence trace: [`docs/coverage/registrant.md`](docs/coverage/registrant.md) — also lists what was deferred (banking columns, SSN blind index, Search route, Guardian scoping). + --- ## What's next -### Immediate next milestone: 0.5 — Registrant vertical slice +### Immediate next milestone: 0.6 — 501-stub routers -Real model, not a stub. Proves the framework end-to-end with a domain entity that has: +Every other entity in ARCH §6.1 gets a model (id + mixins) and a router registered in `main.py` that returns 501 while still firing RBAC + audit. `framework/router.py`'s `_attach_stub` is the starting point. Once real routes run through `make_router`, drop it from the coverage `omit` list in `pyproject.toml`. -- Masked SSN in serializer output (`XXX-XX-1234`) -- App-layer encrypted SSN + banking-placeholder columns -- `@audit_read`-equivalent PII-read audit on GET (via `emit_pii_read`) -- Full CRUD flowing through `CRUDService` with automatic audit + version-history rows -- Migration adding the `registrants` table -- Seed matrix already grants OYP_STAFF / ELIGIBILITY_EXAMINER / PAYROLL_ADMIN / etc. their scoped rights on it (from 0.4) -- Tests: happy paths, SSN masking verified end-to-end via TestClient, PII read logged +### After 0.6 -Coverage doc lands at `docs/coverage/registrant.md`. - -### After 0.5 - -- **0.6** — 501-stub routers for the other 25 entities in ARCH §6.1. Each still fires RBAC + audit. Permission gaps surface at Phase 0 review. - **0.7** — Retention scaffold (nightly job, no-op in Phase 0) - **0.8** — CI pipeline hardening — SAST/SCA/SBOM configured in `.github/workflows/ci.yml` already; 0.8 is the discipline of green builds and any needed suppressions. - **0.9** — Docs + acceptance (README expanded, `docs/nist-mapping.md` skeleton, "Phase 0 done" acceptance checklist) @@ -145,23 +145,9 @@ Each has a POA&M ID. See [`POAM.md`](POAM.md) for the full list of items. These ### Git - Branch: `main` -- Remote: `origin` → `https://gitea.doesworks.net/chris.tonjes/YPMS.git` -- **9 commits ahead of `origin/main`** — push blocked on Gitea auth (interactive prompt). Push from a local terminal: - ```bash - git -C /Users/christonjes/cc/ypms push -u origin main - ``` -- Commit history (newest first): - ``` - 390e917 Mark milestone 0.4 complete in PHASE0_PLAN and POA&M - 913c69c Phase 0 milestone 0.4: RBAC seed with 8 roles + permissions matrix - 0050ad1 Mark milestone 0.3 complete in PHASE0_PLAN and POA&M - 0501850 Phase 0 milestone 0.3: auth + JWT + iam.assurance_level claim - 7379bb4 Mark milestone 0.2 complete in PHASE0_PLAN and POA&M - ba75658 Phase 0 milestone 0.2: vendor OWC framework + YPMS pii_audit - 09d862b Mark milestone 0.1 complete in PHASE0_PLAN and POA&M - 393631a Phase 0 milestone 0.1: repo scaffold + Docker Compose - 8712e93 Initial YPMS planning and ATO artifacts - ``` +- Remote: `origin` → `git@gitea.doesworks.net:chris.tonjes/YPMS.git` (SSH; switched from HTTPS 2026-09-30) +- `main` tracks `origin/main`. Push with `git -C /Users/christonjes/cc/ypms push`. +- Milestone history: `git log --oneline` — one "Phase 0 milestone 0.N" commit per milestone, followed by a "Mark milestone 0.N complete" docs commit. ### Local git identity (repo-local, not global) @@ -182,9 +168,11 @@ git -C /Users/christonjes/cc/ypms config user.email | `security/` (models, rbac) | 80–100% | | `auth/` (security, deps, router, schemas, rate_limit) | 80–100% | | `seed/` (roles, permissions, run) | 77–100% | -| Overall | **92.20%** | +| `youth/` (models, router, schemas) | 99–100% | +| `security/column_crypto.py` | 100% | +| Overall | **94.83%** | -`framework/router.py` is excluded from coverage until it has real routes (0.5) — coverage config in `pyproject.toml`. +`framework/router.py` is still excluded from coverage — Registrant uses a hand-built router, so `make_router` gets its first real callers in 0.6. Coverage config in `pyproject.toml`. --- @@ -256,6 +244,12 @@ The GitHub Actions workflow at `.github/workflows/ci.yml` runs the same commands 8. **Alembic baseline (0001) is empty by design.** All schema lives in 0002+ so that "the framework tables" can be re-derived as an additive layer during future architecture reviews. +9. **`Registrant.ssn` has no getter.** Reading it raises `AttributeError` on purpose. Use `decrypt_ssn()` for the one sanctioned plaintext path, and never put it in a response schema or log line. + +10. **Any new encrypted column must be listed in the model's `__audit_redact__`** — otherwise its ciphertext lands in `audit_log.before/after` and `version_history.snapshot`. + +11. **`COLUMN_ENCRYPTION_KEY` changes make existing ciphertext unreadable.** Phase 0 has no rotation; don't change the dev key on a database you want to keep. `column_crypto._aead` is `lru_cache`d — tests that change the key must `cache_clear()` it and `get_settings`. + --- ## File map @@ -292,13 +286,12 @@ Then the ATO artifacts as needed: --- -## Signals to watch for as we approach milestone 0.5 +## Signals to watch for as we approach milestone 0.6 -- Any need to touch `framework/` or `security/` — resist unless it's a genuine framework improvement that would flow back to OWC. Domain entities live in their own directories. -- Any place where a Registrant handler is tempted to skip `emit_pii_read` — it isn't optional; the audit trail is load-bearing for the ATO story on minors' PII. -- Any temptation to store SSN in cleartext — SSN goes into the app-layer encrypted column; the serializer produces `XXX-XX-1234`; only the `ssn_last4` field is stored plain and only for search/display. -- Any premature row-level RBAC — that's Phase 2+. If a phase-0 handler needs "the caller's own row," it can filter by `actor.id`, but don't build a generic scope layer yet. +- Stub models should be id + mixins only (PHASE0_PLAN §4). Resist adding real fields — that's domain-phase work with its own migration. +- `EducationRecord` and `FosterIndicator` stubs still carry the ELIGIBILITY_EXAMINER-only grants; confirm the 501 routes return 403 (not 501) for OYP_STAFF. +- Any entity that will hold PII in Phase 1+ (Guardian, TaxDocument, Attachment) should get the Registrant treatment then — `pii_read` on reads, `__audit_redact__` on encrypted columns. --- -**End of handoff v0.1.** +**End of handoff v0.2.** diff --git a/POAM.md b/POAM.md index 703f886..14aa3cd 100644 --- a/POAM.md +++ b/POAM.md @@ -176,6 +176,7 @@ Closures retained for traceability. | — | Phase 0 milestone 0.2 shipped (partial closure of YPMS-POAM-040) | 2026-09-30 | OWC framework vendored (base/crud/bulk/pagination/router/version_history + audit + security/rbac + users + auth stub) with owc→ypms renames. YPMS pii_audit decorator added. Pagination bug in upstream OWC fixed here (peek-ahead cursor). 26 tests passing, 93.10% coverage. Migration 0002 creates the framework tables. | | — | Phase 0 milestone 0.3 shipped (partial closure of YPMS-POAM-040) | 2026-09-30 | Local password auth + JWT access/refresh tokens with YPMS-specific claims (`uid`, `roles`, `iam.assurance_level`, `mfa_verified`). Endpoints: /auth/login, /auth/refresh, /auth/me, /auth/change-password. NIST AC-7 lockout at 5 attempts / 15 min (source RFP C.5.7.10). 43 tests passing, 93.47% coverage. | | — | Phase 0 milestone 0.4 shipped (partial closure of YPMS-POAM-040) | 2026-09-30 | Data-driven RBAC seed: 8 roles + permissions matrix (30 entities × 8 actions) at `src/ypms/seed/`. Bootstrap admin auto-seeded (`chris` / DOES_SYSTEM_ADMIN). Loader idempotent + validates cell references. `python -m ypms.seed.run` chained into docker-compose. 53 tests passing, 92.20% coverage. | +| — | Phase 0 milestone 0.5 shipped (partial closure of YPMS-POAM-040) | 2026-09-30 | Registrant vertical slice: model, schemas, router, migration 0003. SSN app-layer encrypted (AES-256-GCM, column-bound AAD) and masked in the serializer (C.5.7.50 / SC-28). `pii_read` audit on every read path (C.5.6.9 #5 / AU-14). Audit + version-history snapshots redact ciphertext via `__audit_redact__`. Portal-only roles limited to own row. 98 tests passing, 94.83% coverage. Evidence trace at `docs/coverage/registrant.md`. | ---