ci / test (push) Has been cancelled
- README rewritten: Docker and native quickstarts, try-it flow, gate commands, promote-a-stub steps. - CONTRIBUTING rules 9-13 (pii_read on PII reads, __audit_redact__ for encrypted columns, retention policy per table, models.py registry, vendored-code change discipline); PR checklist updated. - docs/nist-mapping.md: per-control file:line evidence and remaining gap. - NIST_800_53_CONTROL_MAPPING.md v0.3: Phase 0 notes on 28 rows (12 implemented, 12 partial, 4 progress notes on hybrid/tailored rows) plus a status-shift table. - PHASE0_PLAN: 0.9 complete; Definition of Done 12/14 checked with how each was verified. Open: first green Gitea CI run, Docker/Postgres boot. - POA&M and status handoff updated; status TL;DR no longer claims an unverified Docker boot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
5.9 KiB
Markdown
123 lines
5.9 KiB
Markdown
# YPMS — Youth Program Management System
|
|
|
|
DC Department of Employment Services / Office of Youth Programs — in-house build replacing seven legacy tools (MBSYEP intake, TMS timekeeping, PF3 admin, manual payroll scripts, MicroStrategy, and two supporting apps) with a single Python / FastAPI / Postgres system on Azure Government.
|
|
|
|
**Status:** Phase 0 (foundations) — framework, auth, RBAC, audit, Registrant vertical slice, 501 stubs for every other entity, retention scaffold, CI. See [`PHASE0_STATUS.md`](PHASE0_STATUS.md).
|
|
**System owner + ISSO + AO:** Chris Tonjes, DOES CIO.
|
|
**Full context:** [`OVERVIEW.md`](OVERVIEW.md), [`ARCHITECTURE.md`](ARCHITECTURE.md), [`PHASE0_PLAN.md`](PHASE0_PLAN.md).
|
|
|
|
## Quickstart — Docker (canonical)
|
|
|
|
Requires Docker Desktop (or equivalent). Ports 8000 (app), 5433 (Postgres), 8081 (Adminer) must be free.
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
docker compose up --build
|
|
```
|
|
|
|
Compose runs `alembic upgrade head`, seeds roles + permissions + the bootstrap admin (`chris` / `change-me-immediately`, or `SEED_ADMIN_PASSWORD`), then starts the app.
|
|
|
|
- App: http://localhost:8000 — OpenAPI at http://localhost:8000/docs
|
|
- Adminer: http://localhost:8081 — System PostgreSQL, server `db`, user/password/db `ypms`
|
|
- Stop with `Ctrl+C`; wipe the local DB with `docker compose down -v`.
|
|
|
|
## Quickstart — native, no Docker
|
|
|
|
Uses SQLite in place of Postgres. Fine for poking at the API; Postgres-specific behaviour needs the Docker path.
|
|
|
|
```bash
|
|
python3.12 -m venv .venv && source .venv/bin/activate
|
|
pip install -e '.[dev]'
|
|
|
|
export DATABASE_URL=sqlite:///./ypms-dev.db
|
|
export JWT_SECRET=dev-jwt-secret-change-me-32-bytes-min # >= 32 bytes or the app won't start
|
|
export COLUMN_ENCRYPTION_KEY=dev-col-enc-key-change-me-32bytes-______
|
|
export SEED_ADMIN_PASSWORD=pick-a-local-password
|
|
|
|
alembic upgrade head
|
|
python -m ypms.seed.run
|
|
uvicorn ypms.main:app --reload
|
|
```
|
|
|
|
## Try it
|
|
|
|
```bash
|
|
curl http://localhost:8000/healthz # {"status":"ok"}
|
|
curl http://localhost:8000/readyz # {"status":"ok"} once the DB is reachable
|
|
|
|
TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \
|
|
-d 'username=chris&password=change-me-immediately' | python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])')
|
|
|
|
curl -s -X POST http://localhost:8000/registrants \
|
|
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
|
-d '{"first_name":"Test","last_name":"Youth","dob":"2010-05-01","ssn":"123-45-6789"}'
|
|
# → ssn_masked "XXX-XX-6789"; the SSN itself is never returned
|
|
|
|
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/guardians -H "Authorization: Bearer $TOKEN"
|
|
# → 501 (stub: RBAC checked and audited, no logic yet)
|
|
```
|
|
|
|
Change the bootstrap password right away: `POST /auth/change-password`.
|
|
|
|
The retention job is report-only in Phase 0:
|
|
|
|
```bash
|
|
python -m ypms.framework.retention # logs "0 records to purge" and writes an audit row
|
|
```
|
|
|
|
## Tests and gates
|
|
|
|
```bash
|
|
pytest # SQLite in-memory, ~10s
|
|
pytest --cov=src/ypms --cov-report=term-missing --cov-fail-under=80
|
|
ruff check src tests
|
|
mypy src
|
|
bandit -q -r src -c pyproject.toml
|
|
pip-audit --strict . # runtime dependencies
|
|
```
|
|
|
|
CI runs all of these plus gitleaks and a CycloneDX SBOM on Gitea Actions — [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml).
|
|
|
|
## Repo shape
|
|
|
|
Full tree in [`ARCHITECTURE.md`](ARCHITECTURE.md) §5.1. High points:
|
|
|
|
- `src/ypms/main.py` — app factory; refuses to start if any table lacks a retention policy; opaque 500 handler
|
|
- `src/ypms/settings.py` — pydantic-settings config (secret-length floors enforced)
|
|
- `src/ypms/framework/` — vendored from OWC, plus YPMS additions: `pii_audit.py`, `stub_router.py`, `retention.py`
|
|
- `src/ypms/security/` — RBAC, `column_crypto.py` (AES-256-GCM app-layer encryption)
|
|
- `src/ypms/auth/` — login, JWT (PyJWT), refresh, change-password, login rate limit
|
|
- `src/ypms/youth/` — Registrant, the real vertical-slice entity
|
|
- `src/ypms/stubs.py`, `src/ypms/stub_models.py` — the 26 entities still at 501
|
|
- `src/ypms/retention_classes.py` — PIA §9 retention policy catalog
|
|
- `src/ypms/models.py` — the one list of model modules
|
|
- `src/ypms/seed/` — roles, permissions matrix, bootstrap admin
|
|
- `migrations/` — Alembic; merged revisions pinned in `IMMUTABLE.sha256`
|
|
- `docs/coverage/` — per-entity requirement traces; `docs/nist-mapping.md` — control evidence index
|
|
|
|
## Adding an entity (promoting a stub to real)
|
|
|
|
1. Move the class from `src/ypms/stub_models.py` into `src/ypms/<domain>/models.py`; keep `__tablename__` and `retention_policy`. Add the module to `src/ypms/models.py`.
|
|
2. Add columns in a **new** migration; append its hash to `migrations/IMMUTABLE.sha256`.
|
|
3. Encrypted columns: write through `security.column_crypto`, list them in `__audit_redact__`.
|
|
4. Schemas in `<domain>/schemas.py`; the read schema masks anything sensitive.
|
|
5. Router: `framework.router.make_router` for plain CRUD, or a hand-built router on `CRUDService` (see `youth/router.py`) when you need PII-read audit or row scoping. Register it in `main.py` and remove the entity from `src/ypms/stubs.py`.
|
|
6. Check the permissions matrix rows for the entity — they're policy; review them as such.
|
|
7. Tests, then a coverage doc at `docs/coverage/<entity>.md` (see `registrant.md`).
|
|
|
|
## Contributing
|
|
|
|
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the hard rules.
|
|
|
|
## Compliance
|
|
|
|
- [`SYSTEM_SECURITY_PLAN.md`](SYSTEM_SECURITY_PLAN.md) — NIST 800-53 Moderate SSP
|
|
- [`NIST_800_53_CONTROL_MAPPING.md`](NIST_800_53_CONTROL_MAPPING.md) v0.3 — full control map with Phase 0 evidence
|
|
- [`docs/nist-mapping.md`](docs/nist-mapping.md) — Phase 0 control evidence index (file:line)
|
|
- [`PRIVACY_IMPACT_ASSESSMENT.md`](PRIVACY_IMPACT_ASSESSMENT.md) — PIA covering minors, SSN, banking, FERPA, CFSA
|
|
- [`POAM.md`](POAM.md) — plan of action and milestones
|
|
|
|
## License / usage
|
|
|
|
Internal DOES system. Not for external distribution.
|