diff --git a/backend/app/domain/unit_of_work.py b/backend/app/domain/unit_of_work.py index b93f2b9..f030dc1 100644 --- a/backend/app/domain/unit_of_work.py +++ b/backend/app/domain/unit_of_work.py @@ -10,6 +10,16 @@ Usage in routers:: If an exception propagates, ``__exit__`` issues a rollback automatically. Services should **never** call ``db.commit()``; they use ``db.add()`` / ``db.flush()`` to stage work and let the caller decide when to commit. + +**Documented exceptions** (services that may commit internally): +- ``audit_service.log_action`` — called from 15+ routers; commits to ensure + audit records persist even when callers do not. +- Import services (atomic_import, sigma_import, etc.) — self-contained sync ops. +- Background jobs (campaign_scheduler, intel_service, stale_detection, + mitre_sync) — self-contained operations. +- Self-contained batch ops (e.g. detection_rule_service.auto_associate_rules, + snapshot_service.create_snapshot, campaign_service.generate_campaign_from_*, + osint_enrichment_service.enrich_technique_with_cves). """ from __future__ import annotations diff --git a/backend/app/routers/osint.py b/backend/app/routers/osint.py index 6b16b06..97e0fff 100644 --- a/backend/app/routers/osint.py +++ b/backend/app/routers/osint.py @@ -10,6 +10,7 @@ from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_any_role +from app.domain.unit_of_work import UnitOfWork from app.models.user import User from app.services.osint_enrichment_service import ( enrich_technique_with_cves, @@ -82,12 +83,14 @@ def review_osint_item( user: User = Depends(get_current_user), ): """Mark an OSINT item as reviewed.""" - item = mark_osint_reviewed(db, str(item_id)) - if not item: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="OSINT item not found", - ) + with UnitOfWork(db) as uow: + item = mark_osint_reviewed(db, str(item_id)) + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="OSINT item not found", + ) + uow.commit() return {"id": str(item.id), "reviewed": True} diff --git a/backend/app/routers/scores.py b/backend/app/routers/scores.py index 80dce66..8614191 100644 --- a/backend/app/routers/scores.py +++ b/backend/app/routers/scores.py @@ -11,6 +11,7 @@ from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_role +from app.domain.unit_of_work import UnitOfWork from app.models.user import User from app.services.scoring_service import ( score_technique_by_mitre_id, @@ -127,14 +128,16 @@ def update_scoring_config( Weights are persisted in the database and survive restarts. Validation enforces that all weights are non-negative and sum to 100. """ - result = update_scoring_weights( - db, - tests=payload.tests, - detection_rules=payload.detection_rules, - d3fend=payload.d3fend, - freshness=payload.freshness, - platform_diversity=payload.platform_diversity, - ) + with UnitOfWork(db) as uow: + result = update_scoring_weights( + db, + tests=payload.tests, + detection_rules=payload.detection_rules, + d3fend=payload.d3fend, + freshness=payload.freshness, + platform_diversity=payload.platform_diversity, + ) + uow.commit() from app.services.score_cache import invalidate invalidate() diff --git a/backend/app/routers/worklogs.py b/backend/app/routers/worklogs.py index bb5a5cf..b7a65d8 100644 --- a/backend/app/routers/worklogs.py +++ b/backend/app/routers/worklogs.py @@ -10,6 +10,7 @@ from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_any_role +from app.domain.unit_of_work import UnitOfWork from app.models.user import User from app.services import worklog_service @@ -57,17 +58,20 @@ def create( user: User = Depends(require_any_role("red_tech", "blue_tech", "red_lead", "blue_lead")), ): """Create a manually-logged worklog entry.""" - wl = worklog_service.create_worklog( - db, - entity_type=body.entity_type, - entity_id=body.entity_id, - user_id=user.id, - activity_type=body.activity_type, - started_at=body.started_at, - ended_at=body.ended_at, - duration_seconds=body.duration_seconds, - description=body.description, - ) + with UnitOfWork(db) as uow: + wl = worklog_service.create_worklog( + db, + entity_type=body.entity_type, + entity_id=body.entity_id, + user_id=user.id, + activity_type=body.activity_type, + started_at=body.started_at, + ended_at=body.ended_at, + duration_seconds=body.duration_seconds, + description=body.description, + ) + uow.commit() + db.refresh(wl) return wl diff --git a/backend/app/services/osint_enrichment_service.py b/backend/app/services/osint_enrichment_service.py index 7143735..fcf2b4b 100644 --- a/backend/app/services/osint_enrichment_service.py +++ b/backend/app/services/osint_enrichment_service.py @@ -181,12 +181,10 @@ def get_osint_items_for_technique( def mark_osint_reviewed(db: Session, item_id: str) -> OsintItem | None: - """Mark an OSINT item as reviewed.""" + """Mark an OSINT item as reviewed. Does not commit; caller uses UnitOfWork.""" item = db.query(OsintItem).filter(OsintItem.id == item_id).first() if item: item.reviewed = True - db.commit() - db.refresh(item) return item diff --git a/backend/app/services/scoring_config_service.py b/backend/app/services/scoring_config_service.py index 95cc62a..5103e33 100644 --- a/backend/app/services/scoring_config_service.py +++ b/backend/app/services/scoring_config_service.py @@ -82,9 +82,7 @@ def update_scoring_weights( row.weight_freshness = new.freshness row.weight_platform_diversity = new.platform_diversity - db.commit() - db.refresh(row) - + # Does not commit; caller (router) uses UnitOfWork. return _weights_dict(new) diff --git a/backend/app/services/worklog_service.py b/backend/app/services/worklog_service.py index 7cee816..dfd4dd1 100644 --- a/backend/app/services/worklog_service.py +++ b/backend/app/services/worklog_service.py @@ -39,8 +39,7 @@ def create_worklog( ) wl.integrity_hash = _compute_hash(wl) db.add(wl) - db.commit() - db.refresh(wl) + # Does not commit; caller (router) uses UnitOfWork. return wl