From a6b46f5f762feb99c1694695c135c8ffdfef78ba Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 2 Jul 2026 16:10:22 +0200 Subject: [PATCH] feat(campaigns): add approval fields and CampaignModificationRequest model --- backend/app/models/__init__.py | 4 +-- backend/app/models/campaign.py | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index f36624c..695a324 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -8,7 +8,7 @@ from app.models.threat_actor import ThreatActor, ThreatActorTechnique from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqueMapping from app.models.test_template_detection_rule import TestTemplateDetectionRule from app.models.test_detection_result import TestDetectionResult -from app.models.campaign import Campaign, CampaignTest +from app.models.campaign import Campaign, CampaignTest, CampaignModificationRequest from app.models.compliance import ComplianceFramework, ComplianceControl, ComplianceControlMapping from app.models.coverage_snapshot import CoverageSnapshot, SnapshotTechniqueState from app.models.jira_link import JiraLink, JiraLinkEntityType, JiraSyncDirection @@ -59,7 +59,7 @@ __all__ = [ # Literal argument value "TestTemplateDetectionRule", "TestDetectionResult", # Literal argument value - "Campaign", "CampaignTest", + "Campaign", "CampaignTest", "CampaignModificationRequest", # Literal argument value "ComplianceFramework", "ComplianceControl", "ComplianceControlMapping", # Literal argument value diff --git a/backend/app/models/campaign.py b/backend/app/models/campaign.py index 0d4f8b2..ae06722 100644 --- a/backend/app/models/campaign.py +++ b/backend/app/models/campaign.py @@ -75,6 +75,13 @@ class Campaign(Base): ) start_date = Column(DateTime, nullable=True) # campaign won't activate before this date scheduled_at = Column(DateTime, nullable=True) + approved_by = Column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + ) + approved_at = Column(DateTime, nullable=True) + rejection_reason = Column(Text, nullable=True) # Assign completed_at = Column(DateTime, nullable=True) completed_at = Column(DateTime, nullable=True) # Assign target_platform = Column(String, nullable=True) @@ -229,3 +236,53 @@ class CampaignTest(Base): Index('ix_campaign_tests_campaign', 'campaign_id'), Index('ix_campaign_tests_test', 'test_id'), ) + + +class CampaignModificationRequest(Base): + """A lead's request to add or remove a test from an already-active campaign. + + The underlying ``CampaignTest`` row is only created/deleted once a + manager approves the request — see ``approve_modification_request`` + in ``campaign_crud_service.py``. + """ + __tablename__ = "campaign_modification_requests" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + campaign_id = Column( + UUID(as_uuid=True), + ForeignKey("campaigns.id", ondelete="CASCADE"), + nullable=False, + ) + requested_by = Column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + ) + action = Column(String, nullable=False) # add_test, remove_test + test_id = Column( + UUID(as_uuid=True), + ForeignKey("tests.id", ondelete="CASCADE"), + nullable=False, + ) + order_index = Column(Integer, nullable=True) + phase = Column(String, nullable=True) + justification = Column(Text, nullable=False) + status = Column(String, nullable=False, default="pending") # pending, approved, rejected + reviewed_by = Column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + ) + reviewed_at = Column(DateTime, nullable=True) + review_notes = Column(Text, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + campaign = relationship("Campaign") + test = relationship("Test") + requester = relationship("User", foreign_keys=[requested_by]) + reviewer = relationship("User", foreign_keys=[reviewed_by]) + + __table_args__ = ( + Index("ix_campaign_mod_requests_campaign", "campaign_id"), + Index("ix_campaign_mod_requests_status", "status"), + )