fix(campaigns,tests): admin cannot create campaigns, manager can delete unstarted tests, intensify red/blue team colors
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- Campaign creation and generate-from-threat-actor now use the strict
  role check — admin no longer gets a free pass into campaign content,
  same principle already applied to test-template creation.
- New manager-only DELETE /tests/{id}: removes a standalone test still
  in draft (not started, not linked to any campaign).
- Replaced the orange/indigo stand-ins used across team badges, tabs,
  action buttons, and timers with true red/blue so Red Team and Blue
  Team read unambiguously at a glance.
This commit is contained in:
kitos
2026-07-17 14:34:14 +02:00
parent 82ac9c7014
commit 60ab2e558f
25 changed files with 273 additions and 91 deletions
+8 -3
View File
@@ -25,7 +25,7 @@ from sqlalchemy.orm import Session
from app.database import get_db
# Import get_current_user, require_any_role from app.dependencies.auth
from app.dependencies.auth import get_current_user, require_any_role
from app.dependencies.auth import get_current_user, require_any_role, require_any_role_strict
# Import UnitOfWork from app.domain.unit_of_work
from app.domain.unit_of_work import UnitOfWork
@@ -319,7 +319,10 @@ def create_campaign(
# Entry: db
db: Session = Depends(get_db),
# Entry: current_user
current_user: User = Depends(require_any_role("red_lead", "blue_lead", "manager")),
# Strict variant — admin must NOT get a free pass here. Admin
# administers the site, not campaign content; the only admin path to
# an active campaign is the emergency /activate override below.
current_user: User = Depends(require_any_role_strict("red_lead", "blue_lead", "manager")),
) -> dict:
"""Create a new campaign.
@@ -987,7 +990,9 @@ def generate_campaign_from_actor(
payload: GenerateFromActorPayload = GenerateFromActorPayload(),
db: Session = Depends(get_db),
# Entry: current_user
current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
# Strict variant — admin must NOT get a free pass here, same as the
# plain create_campaign endpoint above.
current_user: User = Depends(require_any_role_strict("red_lead", "blue_lead")),
) -> dict:
"""Auto-generate a campaign from a threat actor's uncovered techniques.
+37
View File
@@ -94,6 +94,11 @@ from app.services.test_crud_service import (
create_test_from_template as crud_create_from_template,
)
# Import from app.services.test_crud_service
from app.services.test_crud_service import (
delete_test as crud_delete_test,
)
# Import from app.services.test_crud_service
from app.services.test_crud_service import (
get_test_detail as crud_get_test_detail,
@@ -543,6 +548,38 @@ def update_test(
return test
@router.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_test(
test_id: uuid.UUID,
db: Session = Depends(get_db),
# manager-only, and admin does NOT get a free pass — this is a
# queue-cleanup action for the manager role specifically, not a site
# administration task.
current_user: User = Depends(require_any_role_strict("manager")),
) -> None:
"""Delete a standalone test that hasn't started yet.
Only tests still in ``draft`` state and not linked to any campaign can
be deleted this way.
Args:
test_id (uuid.UUID): Primary key of the test to delete.
db (Session): SQLAlchemy database session.
current_user (User): Authenticated manager performing the deletion.
"""
with UnitOfWork(db) as uow:
crud_delete_test(db, test_id)
log_action(
db,
user_id=current_user.id,
action="delete_test",
entity_type="test",
entity_id=test_id,
details={},
)
uow.commit()
# ---------------------------------------------------------------------------
# PATCH /tests/{id}/classification — admin data classification
# ---------------------------------------------------------------------------
+22
View File
@@ -438,6 +438,28 @@ def get_test_or_raise(db: Session, test_id: uuid.UUID) -> Test:
return test
def delete_test(db: Session, test_id: uuid.UUID) -> None:
"""Delete a standalone, not-yet-started test from the queue.
Only a test still in ``draft`` (execution hasn't started) and not
linked to any campaign may be deleted this way — a test already being
worked on, or one that's part of a campaign's plan, must be handled
through the normal workflow/campaign-modification paths instead.
Raises EntityNotFoundError, BusinessRuleViolation. Does not commit;
caller commits.
"""
test = get_test_or_raise(db, test_id)
if test.state != TestState.draft:
raise BusinessRuleViolation("Only tests that haven't started yet can be deleted")
in_campaign = db.query(CampaignTest).filter(CampaignTest.test_id == test.id).first()
if in_campaign:
raise BusinessRuleViolation("Cannot delete a test that belongs to a campaign")
db.delete(test)
# Define function get_test_with_technique
def get_test_with_technique(db: Session, test_id: uuid.UUID) -> Test:
"""Fetch a test with technique joined. Raises EntityNotFoundError if not found.