From d85a09719edd230d67ea7a7cb3482c7753915860 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 29 May 2026 00:59:10 +0200 Subject: [PATCH] feat(05-05): add cloud credential cleanup on admin user deletion (SEC-09) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import CloudConnection and get_storage_backend_for_document into admin.py - In delete_user: query all CloudConnection rows for the target user before MinIO cleanup - For each connection: query documents with matching storage_backend, call delete_object best-effort (catch + ignore exceptions — same pattern as MinIO cleanup) - Explicit session.delete(conn) for each CloudConnection row before user row deletion - session.flush() after connection deletes to order SQL before user DELETE - write_audit_log(event_type="cloud.credentials_purged") with providers list metadata - Cloud cleanup runs BEFORE existing MinIO cleanup: credentials still available to build cloud backend instances for delete_object calls (SEC-09) - No orphaned credentials_enc rows after account deletion (SEC-09 satisfied) --- backend/api/admin.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) 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) )