d932134975
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
- New DELETE /users/{id}: hard-deletes a user only if they have zero
activity footprint (no tests, evidence, worklogs, audit entries, Jira
links, procedure/template suggestions, reviewed imports) — otherwise
rejects with a clear message to deactivate instead, to keep the audit
trail intact. Cleans up the user's own notifications/password-setup
tokens as part of deletion. Self-delete is blocked. Wired to a trash
icon in the Users page with a confirm dialog.
- Registered the EvaluationImport model in app/models/__init__.py — it
was missing, so its table never existed in the SQLite test DB; only
surfaced once the new delete-user footprint check queried it.
- Fixed a redirect bug: the axios response interceptor's on-401 redirect
to /login didn't exempt /set-password, so the auth provider's routine
mount-time /auth/me check (401 for a logged-out visitor) bounced anyone
opening their emailed set-password link away before they could use it.
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Admins can permanently delete users with no activity footprint —
|
|
anyone with tests/evidence/worklogs/etc must be deactivated instead, to
|
|
keep the audit trail intact.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def technique(client, auth_headers):
|
|
response = client.post(
|
|
"/api/v1/techniques",
|
|
json={"mitre_id": "T1059", "name": "Test Technique"},
|
|
headers=auth_headers,
|
|
)
|
|
return response.json()
|
|
|
|
|
|
def test_admin_can_delete_user_with_no_footprint(api, auth_headers):
|
|
created = api(
|
|
"post", "/api/v1/users", auth_headers,
|
|
json={"full_name": "Disposable User", "email": "disposable@test.com", "role": "viewer"},
|
|
)
|
|
assert created.status_code == 201, created.text
|
|
user_id = created.json()["id"]
|
|
|
|
resp = api("delete", f"/api/v1/users/{user_id}", auth_headers)
|
|
assert resp.status_code == 204, resp.text
|
|
|
|
get_resp = api("get", f"/api/v1/users/{user_id}", auth_headers)
|
|
assert get_resp.status_code == 404
|
|
|
|
|
|
def test_admin_cannot_delete_own_account(api, auth_headers, admin_user):
|
|
resp = api("delete", f"/api/v1/users/{admin_user.id}", auth_headers)
|
|
assert resp.status_code == 400
|
|
assert "own account" in resp.text.lower()
|
|
|
|
|
|
def test_admin_cannot_delete_user_with_test_footprint(api, auth_headers, technique, red_lead_headers, red_lead_user, client):
|
|
client.cookies.clear()
|
|
test_resp = api(
|
|
"post", "/api/v1/tests", red_lead_headers,
|
|
json={
|
|
"technique_id": technique["id"],
|
|
"name": "Footprint Test",
|
|
"description": "x",
|
|
"platform": "windows",
|
|
},
|
|
)
|
|
assert test_resp.status_code == 201, test_resp.text
|
|
|
|
resp = api("delete", f"/api/v1/users/{red_lead_user.id}", auth_headers)
|
|
assert resp.status_code == 400
|
|
assert "activity history" in resp.text.lower() or "deactivate" in resp.text.lower()
|
|
|
|
# The user must still be there — deactivating is the only allowed path.
|
|
get_resp = api("get", f"/api/v1/users/{red_lead_user.id}", auth_headers)
|
|
assert get_resp.status_code == 200
|
|
|
|
|
|
def test_delete_user_requires_admin(api, red_lead_headers, red_lead_user):
|
|
resp = api("delete", f"/api/v1/users/{red_lead_user.id}", red_lead_headers)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_delete_user_not_found(api, auth_headers):
|
|
resp = api("delete", f"/api/v1/users/{uuid.uuid4()}", auth_headers)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_delete_cleans_up_notifications_and_tokens(api, auth_headers, db):
|
|
created = api(
|
|
"post", "/api/v1/users", auth_headers,
|
|
json={"full_name": "Notifiable User", "email": "notifiable@test.com", "role": "viewer"},
|
|
)
|
|
user_id = uuid.UUID(created.json()["id"])
|
|
|
|
from app.models.notification import Notification
|
|
from app.models.password_setup_token import PasswordSetupToken
|
|
from app.services.notification_service import create_notification
|
|
|
|
create_notification(
|
|
db, user_id=user_id, type="test", title="Hi", message="hi",
|
|
entity_type="test", entity_id=uuid.uuid4(),
|
|
)
|
|
db.add(PasswordSetupToken(user_id=user_id, token="tok-abc", expires_at=__import__("datetime").datetime.utcnow()))
|
|
db.commit()
|
|
|
|
resp = api("delete", f"/api/v1/users/{user_id}", auth_headers)
|
|
assert resp.status_code == 204, resp.text
|
|
|
|
assert db.query(Notification).filter(Notification.user_id == user_id).count() == 0
|
|
assert db.query(PasswordSetupToken).filter(PasswordSetupToken.user_id == user_id).count() == 0
|