feat(05-05): add cloud credential cleanup on admin user deletion (SEC-09)
- 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)
This commit is contained in:
+39
-3
@@ -33,12 +33,12 @@ from pydantic import BaseModel, EmailStr, field_validator
|
|||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.auth import get_current_admin
|
||||||
from deps.db import get_db
|
from deps.db import get_db
|
||||||
from services.audit import write_audit_log
|
from services.audit import write_audit_log
|
||||||
from services.auth import hash_password, revoke_all_refresh_tokens
|
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"])
|
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)
|
_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(
|
docs_result = await session.execute(
|
||||||
select(Document).where(Document.user_id == user_id)
|
select(Document).where(Document.user_id == user_id)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user