diff --git a/PHASE0_STATUS.md b/PHASE0_STATUS.md new file mode 100644 index 0000000..563d51a --- /dev/null +++ b/PHASE0_STATUS.md @@ -0,0 +1,304 @@ +# Phase 0 — Handoff status + +**Last updated:** 2026-09-30 +**Phase:** 0 — Foundations (in flight) +**Owner:** Chris Tonjes, DOES CIO (System Owner / ISSO / AO) +**Companion docs:** [`PHASE0_PLAN.md`](PHASE0_PLAN.md) (scope + milestones), [`ARCHITECTURE.md`](ARCHITECTURE.md) (design), [`README.md`](README.md) (runnable quickstart) + +--- + +## 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. + +--- + +## Current state + +| 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`) | +| 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 | + +--- + +## What's done in Phase 0 + +Read [`PHASE0_PLAN.md`](PHASE0_PLAN.md) for the full scope tables. High points by milestone: + +### 0.1 — Repo scaffold + Docker Compose ✅ + +- `pyproject.toml` (Python 3.12 / FastAPI / SQLAlchemy 2.x / Alembic / Postgres / dev extras: pytest, ruff, mypy, bandit, pip-audit) +- Multi-stage `Dockerfile`, `docker-compose.yml` (app on :8000, Postgres on :5433 to avoid clashing with OWC on :5432, Adminer on :8081) +- `alembic.ini` + empty baseline migration +- `src/ypms/main.py` — FastAPI factory with `/healthz` + `/readyz` +- `src/ypms/settings.py`, `logging.py` (structlog + `sanitize_for_log`), `db.py`, `middleware.py` (RequestId) +- `.github/workflows/ci.yml` — lint, types, tests+80% coverage, bandit, pip-audit, gitleaks +- `README.md`, `CONTRIBUTING.md` + +### 0.2 — Framework vendor-in ✅ + +Vendored from OWC (`../owc-system/src/owc/`, 2026-04-20 snapshot) with `owc → ypms` renames: + +- `framework/{base,bulk,crud,pagination,router,version_history}.py` +- `security/{models,rbac}.py` (Permission table + `can()`/`require_permission()` + `DOES_SYSTEM_ADMIN` super-role) +- `audit/{models,service}.py` (append-only AuditLog + `record()`) +- `users/models.py` (User/Role/UserRole with YPMS additions: `mfa_verified`, `assurance_level`; dropped OWC's `claimant_id`) +- `auth/deps.py` (stub — replaced in 0.3) + +YPMS-specific addition: `framework/pii_audit.py` — `emit_pii_read()` for source RFP `C.5.6.9` #5 / NIST AU-14. + +**Upstream bugfix in the vendored code:** `crud.py` pagination `next_cursor` was the peek-ahead row's id, causing silent row-drop on page boundaries. Fixed here; flagged for OWC. + +Migration `0002_framework_tables.py` creates: `users`, `roles`, `user_roles`, `permissions`, `audit_log`, `version_history`. + +### 0.3 — Auth + JWT + `iam.assurance_level` claim ✅ + +- `src/ypms/auth/security.py` — `hash_password`/`verify_password` (pbkdf2_sha256), `create_access_token` (with YPMS claims), `create_refresh_token`, `decode_token` +- `src/ypms/auth/rate_limit.py` — SlidingWindow; login limiter tuned to NIST AC-7 / source RFP `C.5.7.10`: 5 failed attempts / IP / 15 min → 429 +- `src/ypms/auth/schemas.py` — LoginResponse, RefreshRequest/Response, ChangePasswordRequest, UserRead +- `src/ypms/auth/deps.py` — real `get_current_user` +- `src/ypms/auth/router.py` — `POST /auth/login`, `POST /auth/refresh`, `GET /auth/me`, `POST /auth/change-password` +- Wired into `main.py` + +Access token carries: `sub`, `uid`, `roles`, `iam.assurance_level`, `mfa_verified`, `iat`, `exp`, `type`. + +### 0.4 — RBAC seed loaded ✅ + +- 8 roles defined in `src/ypms/seed/roles.py` +- Permissions matrix at `src/ypms/seed/permissions_matrix.json` — role-first grants (30 entities × 8 actions) +- Loader at `src/ypms/seed/permissions.py` — validates every cell, refuses unknown roles at seed time, idempotent +- Runner at `src/ypms/seed/run.py` — `python -m ypms.seed.run`; auto-seeds `chris` as bootstrap admin +- Docker Compose command now: `alembic upgrade head && python -m ypms.seed.run && uvicorn …` + +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. + +--- + +## What's next + +### Immediate next milestone: 0.5 — Registrant vertical slice + +Real model, not a stub. Proves the framework end-to-end with a domain entity that has: + +- 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 + +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) + +Then Phase 1 (applications + identity + parental consent) starts. + +--- + +## Key decisions locked in + +Documented decisions that a future session must not re-open without a written reason: + +1. **Stack: Python 3.12 / FastAPI / SQLAlchemy 2.x / Postgres** (OWC alignment). Decided 2026-09-30; recorded in [`CLAUDE.md`](CLAUDE.md). +2. **Hosting: Azure Government Cloud** (aligned with OWC and Employer 360). +3. **Chris Tonjes serves as System Owner + ISSO + AO** in Phase 0. Role concentration recorded and accepted in [`SYSTEM_SECURITY_PLAN.md`](SYSTEM_SECURITY_PLAN.md) §5 with rationale (DC agency structure + DC OCTO CISO / USDOL / OIG external oversight + independent SCA and pentest). +4. **Framework vendored from OWC** (not submoduled). Divergences carry written reasons; upstream re-vendoring is a deliberate act. +5. **`DOES_SYSTEM_ADMIN` is the super-role** and does **not** carry permission rows — it bypasses via `security.rbac.ADMIN_ROLE`. +6. **Row-level RBAC is Phase 2+** — Phase 0 is action-on-entity-type only. +7. **Every state-mutating route emits an audit row**; every stub still fires RBAC + audit; sensitive PII reads emit via `emit_pii_read`. +8. **Migrations are immutable once merged** — new revisions, never edits to older ones. +9. **SSL wording in source RFP is treated as TLS 1.2+ (target 1.3)**; FIPS 140-2 is treated as FIPS 140-3 forward alignment; `C.5.7.6` "shared secret answer" is treated as MFA-required. Documented in SSP Appendix D and control-mapping §5. +10. **Data-sharing agreements (OSSE, CFSA, SSA) are renewals of existing DOES agreements**, not new drafts (YPMS-POAM-020, -021, -022). + +--- + +## Open questions + +Each has a POA&M ID. See [`POAM.md`](POAM.md) for the full list of items. These are the ones actively awaiting a decision: + +| # | Question | POA&M | Owner | +|---|---|---|---| +| 1 | What is "Level 3 security" in source RFP `C.5.7.24` — internal maturity vs. NIST 800-63 AAL/IAL 3? | `YPMS-POAM-051` | AO | +| 2 | Youth identity model for minors — social-media login scope; IAL2 proofing before payroll; parental co-auth for under-18? | `YPMS-POAM-050`, `YPMS-POAM-033` | AO + Privacy Officer + OYP | +| 3 | PCI DSS scope — expected out of scope since payroll is ACH via vendor; confirm at vendor selection | `YPMS-POAM-052` | OYP + procurement | +| 4 | Dormant COVID-era health-record CSV path — in-boundary steady state, or defer to emergency-scoped extension? | `YPMS-POAM-053` | AO + Privacy Officer | +| 5 | Retention policy for audit logs, case records, backups — align with DOES records schedule | `YPMS-POAM-054` | DOES records officer | +| 6 | SORN applicability determination | `YPMS-POAM-094` | Privacy Officer + OGC | + +--- + +## Repo state + +### 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 + ``` + +### Local git identity (repo-local, not global) + +``` +Chris Tonjes +``` + +If commits should attribute to a DOES email instead, run: +```bash +git -C /Users/christonjes/cc/ypms config user.email +``` + +### Test / coverage + +| Component | Coverage | +|---|---| +| `framework/` (base, bulk, crud, pagination, pii_audit, version_history) | 91–100% | +| `security/` (models, rbac) | 80–100% | +| `auth/` (security, deps, router, schemas, rate_limit) | 80–100% | +| `seed/` (roles, permissions, run) | 77–100% | +| Overall | **92.20%** | + +`framework/router.py` is excluded from coverage until it has real routes (0.5) — coverage config in `pyproject.toml`. + +--- + +## How to run locally + +### Docker (canonical) + +```bash +cd /Users/christonjes/cc/ypms +cp .env.example .env +docker compose up --build +``` + +- App: http://localhost:8000 (OpenAPI at `/docs`) +- Adminer: http://localhost:8081 (server `db`, user/pw/db all `ypms`) +- On first boot, seeding runs and `chris` is created with password `change-me-immediately` (or `SEED_ADMIN_PASSWORD` env). Change it via `POST /auth/change-password`. + +### Native (no Docker) + +```bash +cd /Users/christonjes/cc/ypms +python3 -m venv .venv && source .venv/bin/activate +pip install -e '.[dev]' +pytest +``` + +Tests use SQLite in-memory — no Postgres required. + +### Log in from the CLI + +```bash +curl -X POST http://localhost:8000/auth/login \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'username=chris&password=change-me-immediately' +``` + +Response includes `access_token`. Use it as `Authorization: Bearer `. + +--- + +## How to verify a milestone before committing + +```bash +source .venv/bin/activate +ruff check src tests # style +mypy src # types (strict) +pytest -q --cov=src/ypms --cov-report=term --cov-fail-under=80 +``` + +The GitHub Actions workflow at `.github/workflows/ci.yml` runs the same commands plus `bandit`, `pip-audit`, and `gitleaks`. + +--- + +## Gotchas — things that would trip up someone new + +1. **`app = create_app()` at the bottom of `src/ypms/main.py` runs at import time.** Tests must have env vars set before that module is imported. `tests/conftest.py` does this at *module* level (not in a fixture) — do not move it back into a fixture. + +2. **SQLite in-memory tests need `StaticPool` + `check_same_thread=False`.** `TestClient` runs FastAPI handlers in a worker thread. The `db` fixture in `tests/conftest.py` configures the engine correctly — don't strip those options. + +3. **`framework/router.py` deliberately does NOT use `from __future__ import annotations`.** FastAPI introspects route parameter types via `typing.get_type_hints`, which would fail to resolve closure-local TypeVars under PEP 563. See the module docstring. + +4. **Two mypy overrides in `pyproject.toml`** — `framework/router.py` (FastAPI closure typevars) and `framework/crud.py` (generic-CRUD TypeVar attribute access). Same limitations OWC accepts. + +5. **The vendored OWC framework had a pagination bug** — `next_cursor` was the peek-ahead row's id, silently dropping one row per page boundary. **Fixed in YPMS's copy** at `framework/crud.py`. If OWC is re-vendored, apply the fix again. + +6. **`DOES_SYSTEM_ADMIN` does NOT appear in `permissions_matrix.json` under `grants`.** It bypasses via `security.rbac.ADMIN_ROLE`. Adding rows for it would be redundant and confuse the "who really has what" story. + +7. **Row-level scoping is a Phase 2+ concern.** `HOST_USER` has `Host.Edit` in the matrix, which means the RBAC check passes. The service layer must ALSO filter by ownership before applying the edit. Don't rely on the matrix alone for row-level authorization. + +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. + +--- + +## File map + +Read in this order if you're picking this up cold: + +1. [`CLAUDE.md`](CLAUDE.md) — project router, distinctions, open questions, resolved decisions +2. [`OVERVIEW.md`](OVERVIEW.md) — programs served, users, integrations, timeline +3. [`ARCHITECTURE.md`](ARCHITECTURE.md) — design blueprint (§4 rationale, §5 layers, §6 data, §12 delivery phases) +4. [`PHASE0_PLAN.md`](PHASE0_PLAN.md) — phase scope, milestone exit criteria, controls-wired-in-Phase-0 +5. **This file** — where we are + what's next + gotchas +6. [`README.md`](README.md) — runnable quickstart +7. [`CONTRIBUTING.md`](CONTRIBUTING.md) — hard rules + +Then the ATO artifacts as needed: + +- [`SYSTEM_SECURITY_PLAN.md`](SYSTEM_SECURITY_PLAN.md) v0.3 — SSP +- [`NIST_800_53_CONTROL_MAPPING.md`](NIST_800_53_CONTROL_MAPPING.md) v0.2 — control mapping +- [`PRIVACY_IMPACT_ASSESSMENT.md`](PRIVACY_IMPACT_ASSESSMENT.md) v0.1 — PIA +- [`POAM.md`](POAM.md) v0.1 — 62 items, 2 closed, 4 partially closed +- [`OLA_RESPONSE_2026-09-30.md`](OLA_RESPONSE_2026-09-30.md) — external Q&A snapshot + +--- + +## POA&M state (as of 2026-09-30) + +- **62 items total** — 2 fully closed, 4 partial closures under `YPMS-POAM-040` +- **6 open decisions** (see Open Questions table above) +- **Immediate remaining action items:** + - PIA route to Privacy Officer + OGC review + - Pull OSSE / CFSA / SSA existing agreements from OGC files to scope renewals + - Draft FIPS 199 categorization memo (`YPMS-POAM-001`) for AO signature + - Schedule DC OCTO security architecture review + +--- + +## Signals to watch for as we approach milestone 0.5 + +- 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. + +--- + +**End of handoff v0.1.**