security: make Campaign mutation audits atomic

This commit is contained in:
2026-07-21 17:56:59 +02:00
parent 60776803c5
commit 0ac903c82d
4 changed files with 331 additions and 48 deletions

View File

@@ -208,6 +208,7 @@ def create_minimal_campaign(
description: str | None = None,
current_flow: str = CampaignVersionFlow.CREATE.value,
current_step: str = "basics",
commit: bool = True,
) -> tuple[Campaign, CampaignVersion]:
existing = session.query(Campaign).filter(Campaign.tenant_id == tenant_id, Campaign.external_id == external_id).one_or_none()
if existing:
@@ -241,8 +242,11 @@ def create_minimal_campaign(
session.flush()
campaign.current_version_id = version.id
session.add(campaign)
_write_campaign_snapshot(version)
session.commit()
if commit:
_write_campaign_snapshot(version)
session.commit()
else:
session.flush()
return campaign, version
@@ -338,6 +342,7 @@ def fork_campaign_version_for_edit(
source_base_path: str | None = None,
autosave: bool = True,
migrate_legacy_mail_settings: bool = False,
commit: bool = True,
) -> CampaignVersion:
"""Create the next sole working version from immutable campaign history.
@@ -407,8 +412,11 @@ def fork_campaign_version_for_edit(
campaign.current_version_id = new_version.id
campaign.status = CampaignStatus.DRAFT.value
session.add(campaign)
_write_campaign_snapshot(new_version)
session.commit()
if commit:
_write_campaign_snapshot(new_version)
session.commit()
else:
session.flush()
return new_version
@@ -466,6 +474,7 @@ def unlock_validated_campaign_version(
tenant_id: str,
campaign_id: str,
version_id: str,
commit: bool = True,
) -> CampaignVersion:
"""Unlock a validation snapshot so it can be edited again.
@@ -515,7 +524,10 @@ def unlock_validated_campaign_version(
campaign.status = CampaignStatus.DRAFT.value
session.add(version)
session.add(campaign)
session.commit()
if commit:
session.commit()
else:
session.flush()
return version
def update_campaign_version(
@@ -534,6 +546,7 @@ def update_campaign_version(
source_base_path: str | None = None,
autosave: bool = False,
migrate_legacy_mail_settings: bool = False,
commit: bool = True,
) -> CampaignVersion:
if raw_json is not None or source_filename is not None or source_base_path is not None:
assert_server_safe_campaign_paths(
@@ -602,8 +615,11 @@ def update_campaign_version(
session.add(version)
session.add(campaign)
session.flush()
_write_campaign_snapshot(version)
session.commit()
if commit:
_write_campaign_snapshot(version)
session.commit()
else:
session.flush()
return version
@@ -616,6 +632,7 @@ def update_campaign_review_state(
inspection_complete: bool,
reviewed_message_keys: list[str],
user_id: str | None,
commit: bool = True,
) -> CampaignVersion:
"""Persist review acknowledgement without mutating the locked campaign data.
@@ -646,7 +663,10 @@ def update_campaign_review_state(
user_id=user_id,
)
session.add(version)
session.commit()
if commit:
session.commit()
else:
session.flush()
return version
@@ -728,6 +748,7 @@ def lock_campaign_version_temporarily(
campaign_id: str,
version_id: str,
user_id: str | None,
commit: bool = True,
) -> CampaignVersion:
"""Apply a reversible user-requested lock without changing workflow state."""
@@ -752,7 +773,10 @@ def lock_campaign_version_temporarily(
version.user_locked_at = datetime.now(UTC)
version.user_locked_by_user_id = user_id
session.add(version)
session.commit()
if commit:
session.commit()
else:
session.flush()
return version
@@ -762,6 +786,7 @@ def unlock_user_locked_campaign_version(
tenant_id: str,
campaign_id: str,
version_id: str,
commit: bool = True,
) -> CampaignVersion:
"""Remove a reversible user lock without invalidating campaign data."""
@@ -785,7 +810,10 @@ def unlock_user_locked_campaign_version(
version.user_locked_at = None
version.user_locked_by_user_id = None
session.add(version)
session.commit()
if commit:
session.commit()
else:
session.flush()
return version
@@ -796,6 +824,7 @@ def permanently_lock_campaign_version(
campaign_id: str,
version_id: str,
user_id: str | None,
commit: bool = True,
) -> CampaignVersion:
"""Apply an irreversible user lock.
@@ -823,7 +852,10 @@ def permanently_lock_campaign_version(
# Retain published_at as a compatibility marker for existing integrations.
version.published_at = version.published_at or now
session.add(version)
session.commit()
if commit:
session.commit()
else:
session.flush()
return version
@@ -834,6 +866,7 @@ def publish_campaign_version(
campaign_id: str,
version_id: str,
user_id: str | None = None,
commit: bool = True,
) -> CampaignVersion:
"""Backwards-compatible alias for the permanent user lock."""
@@ -843,6 +876,7 @@ def publish_campaign_version(
campaign_id=campaign_id,
version_id=version_id,
user_id=user_id,
commit=commit,
)