security: make Campaign mutation audits atomic
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user