fix(users): delete user no longer 500s (clear all FK references)

Deleting a user cleared only Asset/Vulnerability assignments, but users.id is
referenced by four more tables — notification_logs, ai_reports, user_groups,
audit_logs — so Postgres RESTRICT blocked the delete with a 500.

Clear every FK first. Historical rows are NULLed, not deleted, so a removed
user's notifications, AI reports and — critically — AUDIT LOG entries survive
(a user deletion must not erase who-did-what). Group memberships are removed
outright. Then expire the cached relationship state so the User.audit_logs
delete-orphan cascade doesn't re-delete the rows we just detached.
This commit is contained in:
2026-07-22 09:23:37 +02:00
parent d55ebdbec4
commit bf945f838f
+19 -2
View File
@@ -1171,11 +1171,28 @@ async def delete_user(
username = user.username
# Clear user assignments before deletion to avoid FK constraint errors
# Every FK into `users` must be cleared first, or Postgres blocks the
# delete (→ 500). Historical rows are NULLed, not deleted: notifications,
# AI reports and especially the AUDIT LOG must survive so a deleted user's
# actions stay on record (revision-proof). Group memberships are removed
# outright (the assoc row has no meaning without the user).
from app.models.asset import Asset
from app.models.vulnerability import Vulnerability
from app.models.notification_log import NotificationLog
from app.models.ai_report import AIReport
from app.models.audit_log import AuditLog
from app.models.group import user_groups
db.query(Asset).filter(Asset.assigned_user_id == user_id).update({"assigned_user_id": None})
db.query(Vulnerability).filter(Vulnerability.assigned_user_id == user_id).update({"assigned_user_id": None})
db.query(NotificationLog).filter(NotificationLog.user_id == user_id).update({"user_id": None})
db.query(AIReport).filter(AIReport.created_by_id == user_id).update({"created_by_id": None})
db.query(AuditLog).filter(AuditLog.user_id == user_id).update({"user_id": None})
db.execute(user_groups.delete().where(user_groups.c.user_id == user_id))
db.flush()
# Drop cached relationship state so the User.audit_logs delete-orphan
# cascade doesn't re-delete the rows we just detached above.
db.expire(user)
db.delete(user)
db.commit()
@@ -1183,7 +1200,7 @@ async def delete_user(
log_audit_event(
db,
AuditEventType.USER_DELETED,
f"User deleted: {username}",
f"User deleted: {username} (id {user_id})",
user_id=current_user.id,
request=request
)