ci / test (push) Has been cancelled
PHASE0_STATUS also refreshed: SSH remote, push state, 0.6 as next milestone, new gotchas (write-only SSN, __audit_redact__, key changes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
298 lines
16 KiB
Markdown
298 lines
16 KiB
Markdown
# 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.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.
|
||
|
||
---
|
||
|
||
## Current state
|
||
|
||
| Dimension | Status |
|
||
|---|---|
|
||
| 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 |
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
### 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.6 — 501-stub routers
|
||
|
||
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`.
|
||
|
||
### After 0.6
|
||
|
||
- **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` → `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)
|
||
|
||
```
|
||
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% |
|
||
| `youth/` (models, router, schemas) | 99–100% |
|
||
| `security/column_crypto.py` | 100% |
|
||
| Overall | **94.83%** |
|
||
|
||
`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`.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
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
|
||
|
||
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.6
|
||
|
||
- 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.2.**
|