Add PHASE0_STATUS.md — handoff doc
ci / test (push) Has been cancelled

Snapshot of where Phase 0 stands after milestones 0.1-0.4. Structured
as: TL;DR, current state, what's done per milestone, what's next, locked
decisions, open questions, repo state, how to run + verify, gotchas,
file map, POA&M state. Mirrors the DCYWP PHASE_2_STATUS pattern so
anyone (or a future session) can pick this up cold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris TonjesandClaude Opus 4.7 committed 2026-09-30 19:15:20 -04:00
1 parent 390e917de9
commit 185b5d2af3
1 file changed
+304
+304
View File
@@ -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 <ctonjes@gmail.com>
```
If commits should attribute to a DOES email instead, run:
```bash
git -C /Users/christonjes/cc/ypms config user.email <addr>
```
### 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 <token>`.
---
## 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.**