Files
Aegis/backend/tests/test_data_classification.py
T
kitos a3f86c7b31
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
fix(permissions): admin can no longer operate tests — start/submit/edit/create, view only
Admin still bypassed require_any_role (non-strict) on the pure
operator actions: start-execution, submit-red, start-blue-work,
submit-blue, pause/resume-timer, and the red/blue field-edit + general
test create/update/remediation/import-rt/template endpoints all used
the loose dependency even where admin wasn't in the role tuple.
Switched every one of these to require_any_role_strict, matching the
review/validate/dispute/hold/assign endpoints already fixed earlier.

Frontend: removed every remaining role === "admin" disjunct gating
Start Execution, Submit to Blue Team, blue evaluating actions, timer
control, red/blue field editing, test creation, and template creation.
Team-blindness visibility (isBlind) deliberately keeps the admin
bypass — admin still sees both sides unmasked for oversight, since
that's visibility, not operating.

Updated ~20 tests whose fixtures used the admin account as a
convenience shortcut to create/drive tests through the workflow —
switched them to a red_lead account, fixing an incidental
fixture-resolution-order cookie bug along the way (client's cookie
jar prefers the last-logged-in role's cookie over any Bearer header
explicitly passed to a later request).
2026-07-13 09:27:43 +02:00

178 lines
5.9 KiB
Python

"""Tests for data classification fields and edit permissions."""
from unittest.mock import MagicMock
import pytest
from app.models.enums import TestState
from app.models.test import Test
from app.models.technique import Technique
from app.services.test_crud_service import determine_initial_classification
@pytest.fixture
def technique(client, auth_headers):
"""Create a technique for test association (mirrors test_tests.py)."""
response = client.post(
"/api/v1/techniques",
json={"mitre_id": "T9998", "name": "Classification Fixture Technique"},
headers=auth_headers,
)
return response.json()
def _seed_technique(db, tactic="execution") -> Technique:
technique = Technique(
mitre_id="T9999",
name="Test Technique",
tactic=tactic,
platforms=["linux"],
)
db.add(technique)
db.commit()
db.refresh(technique)
return technique
def test_new_test_defaults_to_internal_use_only_via_db_default(db, red_lead_user):
"""A Test() constructed without going through the create_test service
still gets a safe classification via the column's server_default."""
technique = _seed_technique(db)
test = Test(
technique_id=technique.id,
name="Classification test",
created_by=red_lead_user.id,
)
db.add(test)
db.commit()
db.refresh(test)
assert test.data_classification == "internal_use_only"
def test_create_test_endpoint_uses_tactic_heuristic(client, db, api, red_lead_headers, red_lead_user, technique):
"""Going through the real create-test flow applies the tactic-based heuristic."""
resp = api(
"post", "/api/v1/tests", red_lead_headers,
json={"technique_id": technique["id"], "name": "Heuristic test"},
)
assert resp.status_code == 201, resp.text
# The shared `technique` fixture (see conftest/test_tests.py) doesn't set
# a pii-tier tactic, so this should land on the baseline.
assert resp.json()["data_classification"] == "internal_use_only"
def test_admin_cannot_update_classification(client, db, admin_user, admin_token, red_lead_user):
"""Admin administers the site, not test content — classification is a
lead/manager/operator call."""
technique = _seed_technique(db)
test = Test(
technique_id=technique.id,
name="Classify me",
created_by=red_lead_user.id,
state=TestState.draft,
)
db.add(test)
db.commit()
response = client.patch(
f"/api/v1/tests/{test.id}/classification",
json={"data_classification": "pii"},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 403
def test_manager_can_update_classification(client, db, manager_user, manager_headers, red_lead_user):
technique = _seed_technique(db)
test = Test(
technique_id=technique.id,
name="Classify me",
created_by=red_lead_user.id,
state=TestState.draft,
)
db.add(test)
db.commit()
response = client.patch(
f"/api/v1/tests/{test.id}/classification",
json={"data_classification": "pii"},
headers=manager_headers,
)
assert response.status_code == 200
assert response.json()["data_classification"] == "pii"
db.refresh(test)
assert test.data_classification == "pii"
def test_operator_can_update_classification(client, db, admin_user, red_lead_token, red_lead_user):
"""Any test participant (not just admin) can correct the initial classification."""
technique = _seed_technique(db)
test = Test(
technique_id=technique.id,
name="Operator-editable",
created_by=red_lead_user.id,
)
db.add(test)
db.commit()
response = client.patch(
f"/api/v1/tests/{test.id}/classification",
json={"data_classification": "general_use"},
headers={"Authorization": f"Bearer {red_lead_token}"},
)
assert response.status_code == 200
assert response.json()["data_classification"] == "general_use"
def test_viewer_cannot_update_classification(client, db, admin_user, red_lead_user):
"""Viewer is not a test participant and should still be forbidden."""
from app.auth import hash_password
from app.models.user import User
viewer = User(
username="viewer_classif", email="viewer_classif@test.com",
hashed_password=hash_password("x"), role="viewer", is_active=True,
must_change_password=False,
)
db.add(viewer)
db.commit()
login = client.post("/api/v1/auth/login", data={"username": "viewer_classif", "password": "x"})
viewer_token = login.json()["access_token"]
technique = _seed_technique(db)
test = Test(
technique_id=technique.id,
name="Protected",
created_by=red_lead_user.id,
)
db.add(test)
db.commit()
response = client.patch(
f"/api/v1/tests/{test.id}/classification",
json={"data_classification": "pii"},
headers={"Authorization": f"Bearer {viewer_token}"},
)
assert response.status_code == 403
def _technique_stub(tactic):
t = MagicMock()
t.tactic = tactic
return t
def test_determine_initial_classification_defaults_to_internal_use_only():
assert determine_initial_classification(_technique_stub("execution")) == "internal_use_only"
assert determine_initial_classification(_technique_stub(None)) == "internal_use_only"
assert determine_initial_classification(None) == "internal_use_only"
def test_determine_initial_classification_escalates_for_sensitive_tactics():
assert determine_initial_classification(_technique_stub("exfiltration")) == "pii"
assert determine_initial_classification(_technique_stub("collection")) == "pii"
assert determine_initial_classification(_technique_stub("credential-access")) == "pii"
assert determine_initial_classification(_technique_stub("impact")) == "pii"
assert determine_initial_classification(_technique_stub("Exfiltration")) == "pii"