Phase 0 milestone 0.2: vendor OWC framework + YPMS pii_audit
Brings in a working generic-CRUD framework plus its dependencies from OWC
(../owc-system/src/owc/, snapshot 2026-04-20). Every vendored module carries
a header comment naming the source; the OWC → ypms rename is the only
mechanical change.
Vendored modules:
- framework/base.py TimestampMixin, SoftDeleteMixin, VersionedMixin
- framework/pagination.py Page[T] with to_dict()
- framework/version_history.py Snapshot table
- framework/crud.py CRUDService with audit emission
- framework/bulk.py BulkOps (BulkResult / BulkUpdateItem)
- framework/router.py make_router() factory (kept without
`from __future__ import annotations` for
FastAPI type introspection — see the module
docstring)
- audit/models.py AuditLog
- audit/service.py record()
- security/models.py Permission
- security/rbac.py can() + require_permission() dependency
(DOES_SYSTEM_ADMIN as the super-role)
- users/models.py User, Role, UserRole (YPMS adds
mfa_verified and assurance_level; drops
OWC-specific claimant_id)
- auth/deps.py get_current_user() placeholder — real JWT
verify lands in 0.3
YPMS-specific additions on top of the vendored framework:
- framework/pii_audit.py emit_pii_read() — NIST AU-14 / source RFP
C.5.6.9 #5. OWC doesn't need this because
its case-management surface is staff-only;
YPMS must audit reads of minors' PII, SSN,
banking, FERPA-covered OSSE records, and
CFSA foster/ward flags.
Bugfix vs. upstream OWC:
- framework/crud.py Pagination `next_cursor` was previously the
peek-ahead row's id, which combined with
`where id > cursor` on the next page
silently dropped one row per boundary.
Caught by test_list_pagination_forwards_cursor.
Fixed here; noted in the module header so
re-vendoring OWC surfaces it upstream.
Migrations:
- 0002_framework_tables.py Baseline for users, roles, user_roles,
permissions, audit_log, version_history.
Immutable once merged per CONTRIBUTING.md.
- migrations/env.py Imports the new model modules so
autogenerate compares against them.
Tests (26 passing, 93.10% coverage, floor 80%):
- tests/framework/test_crud.py CRUDService lifecycle + snapshots
+ audit emission (7 tests)
- tests/framework/test_bulk.py BulkOps happy paths + failure
reporting (4 tests)
- tests/framework/test_pagination.py Page.to_dict serializer contract
(2 tests)
- tests/framework/test_pii_audit.py emit_pii_read row shape,
anonymous case (2 tests)
- tests/security/test_rbac.py can() + admin bypass + role union
(4 tests)
- tests/test_readyz.py /readyz smoke (1 test)
- tests/test_logging.py sanitize_for_log contract (3 tests)
Green locally: ruff clean, mypy clean (with a targeted override on
framework/router.py and framework/crud.py for TypeVar limitations that
OWC also accepts).
Advances YPMS-POAM-040. Prepares milestone 0.3 (JWT auth) and 0.4 (RBAC
seed) to plug into an already-live framework.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
09d862bf4a
commit
ba756586aa
31 files changed
+1493
-11
No files matched your search
@@ -0,0 +1,35 @@
|
||||
"""PII-read audit emission (YPMS-specific framework addition)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ypms.audit.models import AuditLog
|
||||
from ypms.framework.pii_audit import PII_READ_ACTION, emit_pii_read
|
||||
from ypms.users.models import User
|
||||
|
||||
|
||||
def test_emit_pii_read_records_expected_row(db: Session) -> None:
|
||||
actor = User(username="reader", email="r@example.com", password_hash="x")
|
||||
db.add(actor)
|
||||
db.flush()
|
||||
|
||||
emit_pii_read(db, actor=actor, entity="Registrant", entity_id=42, ip="10.0.0.1")
|
||||
|
||||
row = db.query(AuditLog).one()
|
||||
assert row.action == PII_READ_ACTION
|
||||
assert row.entity == "Registrant"
|
||||
assert row.entity_id == 42
|
||||
assert row.actor_id == actor.id
|
||||
assert row.actor_username == "reader"
|
||||
assert row.ip == "10.0.0.1"
|
||||
assert row.before is None
|
||||
assert row.after is None
|
||||
|
||||
|
||||
def test_emit_pii_read_allows_anonymous(db: Session) -> None:
|
||||
emit_pii_read(db, actor=None, entity="Registrant", entity_id=99)
|
||||
row = db.query(AuditLog).one()
|
||||
assert row.actor_id is None
|
||||
assert row.actor_username is None
|
||||
assert row.entity_id == 99
|
||||
Reference in new issue
Block a user