Files
YPMS/PHASE0_PLAN.md
T
Chris TonjesandClaude Opus 5.5 7d11f2404f
ci / test (push) Has been cancelled
Mark milestone 0.6 complete in PHASE0_PLAN, POA&M, and status handoff
Status handoff also records four permission-matrix review items the
stub sweep surfaced (no Restore holders; admin-only Program /
EligibilityRule config; admin-only IntegrationEvent; HOST_USER Delete
pending row-level ownership).

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

23 KiB
Raw Blame History

YPMS — Phase 0 Plan

Phase: 0 — Foundations (runnable local skeleton) Depends on: ARCHITECTURE.md v0.1, SYSTEM_SECURITY_PLAN.md v0.3, POA&M v0.1 Stack: Python 3.12 · FastAPI · SQLAlchemy 2.x · Alembic · Postgres 16 (SQLite in tests) · Docker Compose · pytest — matches OWC No frontend. Phase 0 is API + minimal admin shell. Public portal is Phase 1+.


1. Phase 0 scope

Phase 0 delivers a runnable local repo that every future phase bolts onto. It does not implement domain logic (eligibility rules, matching, timekeeping, payroll, integrations) — that's Phase 1+.

In scope

Area What ships
Repo scaffold Monorepo layout matching ARCHITECTURE.md §5.1: src/ypms/, tests/, migrations/, docs/. pyproject.toml, Dockerfile, docker-compose.yml, alembic.ini, .env.example. docker compose up boots app + Postgres.
Framework vendor-in Copy OWC framework/ and security/ modules into src/ypms/framework/ and src/ypms/security/. Header comment names the OWC commit; if a YPMS-specific tweak is needed it stays in YPMS's copy and does not flow back.
Config, logging, health pydantic-settings-based config, structured JSON logging (sensitive-field sanitizer per ARCH §10.1), /healthz and /readyz.
Auth Local username + password (pbkdf2_sha256) + JWT. iam.assurance_level claim issued at authentication time (§4 of ARCH). No external IdPs in Phase 0.
RBAC seed Data-driven permissions matrix seeded from src/ypms/seed/permissions_matrix.json. Roles seeded: DOES_SYSTEM_ADMIN (super-role, bypasses checks), OYP_STAFF, ELIGIBILITY_EXAMINER, PAYROLL_ADMIN, REPORTING_ANALYST, HOST_USER, PARENT_USER, YOUTH_USER.
Generic Entity base CRUD + soft delete + optimistic locking + audit + version history + cursor pagination + bulk create/update/delete. Vendored from OWC.
Audit logging Framework-emitted for every state change (create / update / delete / restore). Structured JSON, append-only table. Ties to NIST AU-2 / AU-3 / AU-12 and source RFP C.5.7.39, C.5.7.48, C.5.7.49.
PII-read audit decorator YPMS addition: @audit_read("pii_access") on sensitive-record read endpoints. Ties to source RFP C.5.6.9 #5. Exercised on the Phase 0 Registrant vertical slice.
Registrant vertical slice The Registrant entity is real, end-to-end: model, schemas, router, migration, permissions, tests, coverage doc. Proves the framework works with a domain entity that has masked SSN, encrypted-at-rest banking placeholder, and PII-read audit.
501 stubs for other entities Every entity enumerated in ARCH §6.1 (Guardian, Host, Worksite, Position, Program, Application, Enrollment, TimeEntry, Timesheet, PayrollBatch, TransportationVoucher, Attachment, Note, IntegrationEvent, Notification, EducationRecord, FosterIndicator, ConsentRecord, Cohort, EligibilityRule, JobPosting, HostContact, Certification, Orientation, TaxDocument) shipped as a 501-returning router that still fires RBAC + audit. Permission gaps surface at Phase 0, not Phase N.
Retention scaffold retention_policy attribute declared on entity models (per ARCH §6.4). Nightly job wired but no-op in Phase 0 — enforcement lands with the entity-specific implementations in later phases.
Alembic baseline One initial migration containing all model tables. Phase 0 baseline is 0001_baseline.py; immutable once merged.
CI-ready layout GitHub Actions workflow (matches OWC): lint (ruff), type check (mypy or pyright — match OWC), tests (pytest), SAST (bandit), SCA (pip-audit), secret scan (gitleaks), SBOM (CycloneDX). Blocks merge on failure.
Test discipline ≥ 80% coverage on framework/ and security/. Every other module needs at least one happy-path test.
Docs README.md (runnable quickstart, entity-adding template), CONTRIBUTING.md (hard rules, mirrored from OWC), PHASE0_PLAN.md (this file), docs/nist-mapping.md (stub).

Out of scope (Phase 1+)

  • Any frontend. Public portal, host portal, staff portal — all Phase 1+. The Phase 0 admin surface is FastAPI's built-in /docs (OpenAPI UI).
  • External IdP integration. Entra ID (staff), Login.gov (youth/host), social-media OAuth (browsing) — all Phase 1.
  • Parental co-consent lifecycle. Data model has a Guardian stub and a ConsentRecord stub; the workflow that binds them to an Application is Phase 1 alongside intake.
  • Integrations. SSA, OSSE, CFSA, DC MAR, payroll vendor, LMS — Phase 2 or later depending on dependency.
  • Eligibility engine. Rules seed, engine, decision workflow — Phase 2.
  • Timekeeping and payroll. Phase 7.
  • Reporting. MicroStrategy replacement is Phase 8.
  • Retention automation execution. Scaffold only; enforcement per entity as entities gain real fields.
  • Row-level RBAC. Phase 0 is "can role X do action Y on entity type Z?" Row-level ("only your caseload") is Phase 2+.
  • Real PII. Dev and test are synthetic only. Sanitization pipelines for prod-refresh come with Phase 5 CSV-ingest work.

2. Traceability

Every Phase 0 component maps to a source-RFP requirement or a NIST control. Table is the audit trail — mirrors OWC's PROJECT_PLAN.md §2 pattern.

Component Source Notes
User model + role seed Source RFP C.5.7.29–C.5.7.34; NIST AC-2 8 initial roles; seeded at migration time
RBAC permission check Source RFP C.5.7.27, C.5.7.32; NIST AC-3, AC-6 Data-driven; matrix loaded from seed JSON
Generic CRUD framework Source RFP C.5.21.31–C.5.21.34 (define, view, comment on, view history of cases and tasks); vendored from OWC Build once, reuse
Audit log Source RFP C.5.7.39, C.5.7.48, C.5.7.49, C.5.6.9 #4, #8, #11, #12, #13, #17, #18; NIST AU-2, AU-3, AU-12 Structured JSON, append-only table
PII-read audit decorator Source RFP C.5.6.9 #5; NIST AU-14 YPMS-specific addition above OWC's framework
Soft delete + version history Source RFP implicit (case/task history retention C.5.21.34) Common base mixin
Auth + session mgmt Source RFP C.5.7.1, C.5.7.3–C.5.7.14, C.5.7.38; NIST IA-2, IA-4, IA-5 Local + JWT; iam.assurance_level claim for future assurance gates
Password hashing Source RFP C.5.7.9; NIST IA-5(1) pbkdf2_sha256; FIPS 140-3 by container of the library where available
Config / secrets handling NIST SC-12, SC-28, CM-6 Env-based; .env.example committed, .env not; Key Vault comes Phase 1
Registrant vertical slice Source RFP C.5.3.1, C.5.3.4 (youth application, account management); ARCH §6.1 Proves the framework end-to-end
501-stubs on remaining entities Source RFP C.5.7.27 (enforcement even on unimplemented routes) RBAC + audit fire regardless
Alembic migration discipline NIST CM-3, CM-4; source RFP C.5.8.12 (impact analysis before DB change) Migrations immutable once merged (OWC hard rule)

3. Milestones

# Milestone Exit criteria
0.1 Repo scaffold + Docker Compose boots ✅ Complete 2026-09-30. Tree matches ARCH §5.1 (Phase 0 subset), docker-compose.yml boots app+Postgres+Adminer, pytest green (3 tests), ruff clean, mypy clean. Docker boot verified via config; live end-to-end verification on next docker-enabled workstation.
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 ✅ 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 ✅ Complete 2026-09-30. 26 stub entities registered in src/ypms/stubs.py, each with a router from framework/stub_router.py exposing one route per matrix action (8). Every route runs the real RBAC check and writes an audit row before returning 501. tests/test_stubs.py sweeps entity × role × route against the matrix JSON (501 + one audit row when granted; 403 + none when not). Registrant restore moved to the matrix Restore action. 138 tests, 95.05% coverage.
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.
0.9 Docs + acceptance README.md with runnable quickstart; CONTRIBUTING.md with hard rules; docs/nist-mapping.md skeleton; this file marked complete.

Each milestone independently mergeable.


4. Data-model sketch (Phase 0)

Minimal. Real fields on Registrant because it's the vertical slice; everything else is a stub with id, created_at, updated_at, deleted_at, version — enough to demonstrate framework mixins.

Registrant                                        (real — vertical slice)
  ├── id
  ├── first_name, middle_name, last_name
  ├── dob
  ├── ssn_encrypted            (app-layer encrypted; masked in serializer to XXX-XX-1234)
  ├── ssn_last4                (denormalized for search / display)
  ├── email, phone
  ├── address_line1, line2, city, state, zip
  ├── minor_flag               (derived from dob at read time)
  ├── created_at, updated_at, deleted_at, version   (from framework mixins)

User                                              (real — needed for auth)
  ├── id, username, password_hash, is_active
  ├── roles (M2M → Role via RoleAssignment)
  ├── mfa_verified             (Phase 0: always false; Phase 1 flips to real)
  ├── assurance_level          ('standard' by default; upgraded by Phase 1 flows)

Role                                              (real — seed-loaded)
  ├── id, name, description, is_superuser

Permission                                        (real — seed-loaded)
  ├── id, entity, action

RoleAssignment                                    (real — junction)
  ├── role_id, permission_id

AuditRow                                          (real — framework)
  ├── id, actor_id, action, entity, entity_id
  ├── request_id, before (JSONB), after (JSONB), timestamp

All other entities from ARCH §6.1                 (stubs — id + mixins only)
  Guardian · ConsentRecord · Host · HostContact · Worksite · Position ·
  JobPosting · Program · Cohort · EligibilityRule · Application ·
  EligibilityDecision · Certification · Orientation · Enrollment · TimeEntry ·
  Timesheet · PayrollBatch · TaxDocument · TransportationVoucher · Attachment ·
  Note · IntegrationEvent · Notification · EducationRecord · FosterIndicator

5. Repository layout at Phase 0 end

Matches ARCH §5.1 with only the Phase 0 subset populated:

ypms/
├── README.md                        (runnable quickstart)
├── CONTRIBUTING.md                  (hard rules mirrored from OWC)
├── PHASE0_PLAN.md                   (this file)
├── ARCHITECTURE.md                  (design blueprint)
├── ... (SSP, control mapping, POA&M, PIA, TODO, CLAUDE.md, OVERVIEW.md)
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
├── alembic.ini
├── .env.example
├── .github/workflows/ci.yml
├── migrations/versions/0001_baseline.py
├── src/ypms/
│   ├── __init__.py
│   ├── main.py                      (FastAPI factory + router registration)
│   ├── settings.py                  (pydantic-settings)
│   ├── logging.py                   (structured JSON + sensitive-field sanitizer)
│   ├── db.py                        (engine, session, base)
│   ├── deps.py                      (FastAPI deps)
│   ├── framework/                   (vendored from OWC)
│   │   ├── __init__.py
│   │   ├── entity.py                (base + mixins)
│   │   ├── crud.py                  (CRUDService with audit emission)
│   │   ├── router.py                (make_router factory)
│   │   ├── audit.py                 (framework audit emitter)
│   │   ├── pii_audit.py             (YPMS addition — @audit_read decorator)
│   │   ├── pagination.py
│   │   ├── retention.py             (scaffold; no-op in Phase 0)
│   │   └── seed_loader.py
│   ├── security/                    (vendored + YPMS additions)
│   │   ├── __init__.py
│   │   ├── auth.py                  (JWT create/verify with iam.assurance_level)
│   │   ├── passwords.py             (pbkdf2_sha256)
│   │   ├── rbac.py                  (can/require_permission decorators)
│   │   └── encryption.py            (app-layer column encryption helpers)
│   ├── seed/
│   │   └── permissions_matrix.json
│   ├── youth/                       (Registrant — real)
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   └── service.py               (thin service using CRUDService)
│   ├── parent/                      (Guardian — stub)
│   ├── host/                        (Host, HostContact, Worksite, Position, JobPosting — all stubs)
│   ├── program/                     (Program, Cohort, EligibilityRule — stubs)
│   ├── application/                 (Application, EligibilityDecision, Certification, ConsentRecord — stubs)
│   ├── enrollment/                  (Enrollment — stub)
│   ├── orientation/                 (Orientation — stub)
│   ├── timekeeping/                 (TimeEntry, Timesheet — stubs)
│   ├── payroll/                     (PayrollBatch, TaxDocument, TransportationVoucher — stubs)
│   ├── notifications/               (Notification — stub)
│   ├── integrations/                (IntegrationEvent — stub; no adapter modules yet)
│   ├── attachments/                 (Attachment, Note — stubs)
│   ├── education_records/           (EducationRecord — stub; FERPA-scoped)
│   ├── foster_indicators/           (FosterIndicator — stub; Title 4-scoped)
│   └── admin/                       (User, Role, Permission — real; system-admin endpoints)
├── tests/
│   ├── framework/                   (vendored tests + YPMS additions for pii_audit)
│   ├── security/                    (auth, rbac, encryption)
│   ├── youth/                       (Registrant e2e)
│   ├── stubs/                       (one test per stub entity: RBAC + audit fire on 501)
│   └── conftest.py
└── docs/
    ├── nist-mapping.md              (skeleton — populated per phase)
    ├── coverage/
    │   └── registrant.md
    └── compliance/                  (mirrors OWC docs/compliance/ once real controls have code)

6. NIST controls that get real (non-stub) code in Phase 0

Following OWC's pattern of only wiring controls that code actually implements. Others stay as 📝 in the control mapping until later phases.

Control Implementation
AC-2 User model, role-based accounts, admin can enable/disable
AC-3 require_permission decorator on every mutating route; 501 stubs still enforce
AC-6 RBAC via permissions matrix; DOES_SYSTEM_ADMIN super-role explicit
AC-7 5-failure lockout — Phase 0 targets the check; actual lockout with locked_until field lands here
AC-11 / AC-12 JWT expiry (short-lived access); refresh token flow lands Phase 1
AU-2 / AU-3 / AU-12 Framework audit emission on every state change; AuditRow model + emission tests
AU-8 UTC-normalized timestamps everywhere; Postgres timezone set to UTC in Docker Compose
AU-14 PII-read audit decorator on Registrant read endpoints
CM-3 Alembic migrations immutable once merged (repo policy + CI check)
IA-2 Authentication required for every non-public route
IA-4 Unique usernames; no re-issue on delete (deleted users tombstoned, username reserved)
IA-5 Password change endpoint; pbkdf2_sha256
IA-5(1) FIPS-approved password hashing
SC-8 Dev/test uses TLS via reverse-proxy in compose (self-signed OK for local); production TLS is deploy-phase
SC-12 / SC-13 / SC-28 App-layer column encryption for ssn_encrypted and banking placeholder using cryptography-library primitives; Key Vault swap-in is Phase 1
SI-10 Pydantic schema validation on all inbound requests
SI-11 Non-exploitable error responses (custom exception handler)

Everything else in the control mapping stays 📝 until later phases wire it.


7. Risks

Risk Mitigation
Vendoring OWC's framework introduces divergence over time Header-comment naming the OWC commit; document the vendored-code policy in CONTRIBUTING. Pulling upstream OWC changes is a deliberate act, not an ambient sync.
Data-driven RBAC seed drifts from code expectations Seed loaded at startup with schema validation; missing entity or action fails startup rather than silently ignoring.
501 stubs mask real permission gaps Every stub is tested to fire RBAC + audit; gap surfaces in tests, not later.
App-layer encryption in Phase 0 uses local key material .env value in dev/test only; documented; Phase 1 swap to Key Vault is a single-file change with the encryption helper interface preserved. Local keys never used in staging or prod.
Vertical slice on Registrant hides framework issues for other entity shapes 501 stubs still exercise the framework at the RBAC + audit level for every entity, catching factory-parameter mistakes.
Alembic baseline captures wrong shape and every future migration inherits the mistake Baseline reviewed against ARCH §6 by CIO before first merge; every downstream migration is additive, so wrong shapes are correctable via new migration (never edit baseline).
CI pipeline slow enough to block iteration Test suite must stay under 60s (SQLite fixtures). Framework tests get priority; slow integration-style tests deferred to a nightly workflow.
Retention scaffold gets forgotten and never enforces Every entity model asserts a retention_policy in a startup check; entity without one fails startup. Enforcement per class lands later, but the scaffold is unforgettable.

8. Definition of done for Phase 0

Phase 0 is complete when:

  • docker compose up boots app + Postgres cleanly; /healthz returns 200.
  • alembic upgrade head applies the baseline migration.
  • POST /auth/login issues a JWT with iam.assurance_level claim.
  • POST /registrants creates a Registrant with masked SSN in the response, emits an audit row, requires OYP_STAFF or higher.
  • GET /registrants/{id} reads the record, fires the PII-read audit decorator, masks SSN, requires appropriate role.
  • PATCH /registrants/{id} updates fields, emits before/after in audit row, requires appropriate role.
  • DELETE /registrants/{id} soft-deletes, emits audit, requires appropriate role.
  • Every stub entity's endpoints return 501 but still emit an audit row and enforce RBAC.
  • Tests green in CI: lint, types, pytest, SAST, SCA, secret scan, SBOM.
  • Coverage ≥ 80% on framework/ and security/; every stub has one test.
  • README.md quickstart works on a clean checkout.
  • CONTRIBUTING.md published with hard rules mirrored from OWC.
  • docs/nist-mapping.md lists the controls wired above, with file:line evidence.
  • Control mapping NIST_800_53_CONTROL_MAPPING.md updated: the Phase-0-wired controls flip from 📝 to ✅ (or 🟡 for partials); file:line evidence populated.

9. What I need from you

  1. Match OWC's Azure Gov deployment target? OWC currently runs Docker locally; its Azure Gov deployment is Phase 19/20. YPMS Phase 0 is local-only too. Confirm we're not provisioning Azure Gov in Phase 0 — Azure comes with Phase 1 (Key Vault swap-in) or later.
  2. Match OWC's CI provider? OWC uses [TBD — confirm]. I'll default to GitHub Actions unless OWC is on Azure DevOps; then I match. Say which.
  3. 8 initial roles OK, or want a different seed set? Current default: DOES_SYSTEM_ADMIN, OYP_STAFF, ELIGIBILITY_EXAMINER, PAYROLL_ADMIN, REPORTING_ANALYST, HOST_USER, PARENT_USER, YOUTH_USER. Missing roles surface fast at Phase 1; adding one later is a seed update.
  4. First entity as Registrant, or something smaller? Registrant is the natural vertical slice because everything else hangs off it. Smaller options: Program (mostly reference data) or HostContact (simple). I lean Registrant because the encryption + masking + PII-read audit path is what Phase 0 needs to prove.
  5. Test-runner + type-checker to match OWC? Confirm: pytest for tests, and pick mypy or pyright to match OWC.

If you just say "proceed" I'll:

  • Start with milestone 0.1 (repo scaffold + Docker Compose),
  • Default GitHub Actions, mypy, the 8 roles above, Registrant as the slice, and no Azure Gov in Phase 0,
  • Merge each milestone independently as it lands.

End of draft v0.1.