- 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>
This commit is contained in:
1 parent
99a50dc86c
commit
e3c4cb0cdc
7 files changed
+241
-111
No files matched your search
@@ -2,11 +2,11 @@
|
||||
|
||||
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:** Pre-development. Phase 0 (foundations) in flight.
|
||||
**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 (local)
|
||||
## Quickstart — Docker (canonical)
|
||||
|
||||
Requires Docker Desktop (or equivalent). Ports 8000 (app), 5433 (Postgres), 8081 (Adminer) must be free.
|
||||
|
||||
@@ -15,68 +15,107 @@ cp .env.example .env
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
App at http://localhost:8000. OpenAPI docs at http://localhost:8000/docs.
|
||||
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
|
||||
curl http://localhost:8000/healthz # → {"status":"ok"}
|
||||
curl http://localhost:8000/readyz # → {"status":"ok"} once db is up
|
||||
```
|
||||
|
||||
Adminer (DB explorer) at http://localhost:8081. Connect with:
|
||||
- System: PostgreSQL
|
||||
- Server: `db`
|
||||
- Username / Password / Database: `ypms` / `ypms` / `ypms`
|
||||
|
||||
Stop with `Ctrl+C`; wipe local DB with `docker compose down -v`.
|
||||
|
||||
## Tests
|
||||
|
||||
Local (no Docker required for the tests):
|
||||
|
||||
```bash
|
||||
python -m venv .venv && source .venv/bin/activate
|
||||
python3.12 -m venv .venv && source .venv/bin/activate
|
||||
pip install -e '.[dev]'
|
||||
pytest
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Coverage:
|
||||
## Try it
|
||||
|
||||
```bash
|
||||
pytest --cov=src/ypms --cov-report=term-missing
|
||||
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)
|
||||
```
|
||||
|
||||
Tests use SQLite in-memory for speed. Postgres-specific behaviour gets its own Postgres-only fixture when a migration or query needs it (see [`ARCHITECTURE.md`](ARCHITECTURE.md) §4.8).
|
||||
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` — FastAPI factory
|
||||
- `src/ypms/settings.py` — pydantic-settings config
|
||||
- `src/ypms/db.py` — SQLAlchemy engine, session, `Base`
|
||||
- `src/ypms/logging.py` — structlog + sensitive-field sanitizer
|
||||
- `src/ypms/middleware.py` — request id
|
||||
- `src/ypms/framework/` — vendored from OWC, milestone 0.2
|
||||
- `src/ypms/security/` — auth + RBAC + column encryption, milestones 0.3–0.4
|
||||
- `src/ypms/youth/` — Registrant entity (vertical slice), milestone 0.5
|
||||
- Other entity packages — 501 stubs, milestone 0.6
|
||||
- `migrations/` — Alembic, immutable-once-merged
|
||||
- `tests/` — pytest, SQLite by default
|
||||
- `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
|
||||
## Adding an entity (promoting a stub to real)
|
||||
|
||||
Once the framework lands in milestone 0.2, adding a new entity follows the OWC add-entity pattern (model → schemas → router → main.py registration → migration → permissions seed entry → test → coverage doc). Full instructions land here at milestone 0.2 completion.
|
||||
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. Highlights: migrations are immutable once merged, every state-mutating route emits an audit row, framework/security modules must not carry domain logic, no `--no-verify` or `# type: ignore` without a written reason.
|
||||
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the hard rules.
|
||||
|
||||
## Compliance
|
||||
|
||||
- [`SYSTEM_SECURITY_PLAN.md`](SYSTEM_SECURITY_PLAN.md) v0.3 — NIST 800-53 Moderate SSP
|
||||
- [`NIST_800_53_CONTROL_MAPPING.md`](NIST_800_53_CONTROL_MAPPING.md) v0.2 — full control-family map
|
||||
- [`PRIVACY_IMPACT_ASSESSMENT.md`](PRIVACY_IMPACT_ASSESSMENT.md) v0.1 — PIA covering minors, SSN, banking, FERPA, CFSA
|
||||
- [`POAM.md`](POAM.md) v0.1 — POA&M with 62 items and 2 closures as of 2026-09-30
|
||||
- [`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
|
||||
|
||||
|
||||
Reference in new issue
Block a user