diff --git a/backend/api/admin.py b/backend/api/admin.py index b08a83a..4b538b2 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -33,12 +33,12 @@ from pydantic import BaseModel, EmailStr, field_validator from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from db.models import Document, Quota, RefreshToken, Topic, User +from db.models import CloudConnection, Document, Quota, RefreshToken, Topic, User from deps.auth import get_current_admin from deps.db import get_db from services.audit import write_audit_log from services.auth import hash_password, revoke_all_refresh_tokens -from storage import get_storage_backend +from storage import get_storage_backend, get_storage_backend_for_document router = APIRouter(prefix="/api/admin", tags=["admin"]) @@ -488,7 +488,43 @@ async def delete_user( _ip = request.headers.get("X-Forwarded-For") or (request.client.host if request.client else None) - # SEC-09: collect all user documents and delete MinIO objects BEFORE DB delete + # SEC-09 (cloud): purge cloud-stored documents and credentials BEFORE DB delete. + # Must run before MinIO cleanup so that credentials are still available to build + # the cloud backend instances for delete_object calls. + cloud_conns_result = await session.execute( + select(CloudConnection).where(CloudConnection.user_id == user_id) + ) + cloud_conns = cloud_conns_result.scalars().all() + for conn in cloud_conns: + # Delete cloud objects stored in this provider for this user + cloud_docs_result = await session.execute( + select(Document).where( + Document.user_id == user_id, + Document.storage_backend == conn.provider, + ) + ) + for doc in cloud_docs_result.scalars().all(): + try: + backend = await get_storage_backend_for_document(doc, user, session) + await backend.delete_object(doc.object_key) + except Exception: + pass # Best-effort cloud object cleanup; deletion proceeds regardless + # Purge the credentials row (FK cascade would also remove it, but explicit + # deletion here guarantees credentials_enc is gone before commit — SEC-09) + await session.delete(conn) + if cloud_conns: + await session.flush() # Flush connection deletes before user delete + await write_audit_log( + session, + event_type="cloud.credentials_purged", + user_id=user_id, + actor_id=_admin.id, + resource_id=user_id, + ip_address=_ip, + metadata_={"providers": [c.provider for c in cloud_conns]}, + ) + + # SEC-09 (minio): collect all user documents and delete MinIO objects BEFORE DB delete docs_result = await session.execute( select(Document).where(Document.user_id == user_id) )