fix(vector-sync): address round-3 review — gate purge route, bound set, nits

- app.py: register /api/v1/vector-sync/purge only when vector_sync_enabled, so
  it returns 404 (not a 500 from get_qdrant_client) when sync is off
- scanner: bound _consent_backstop_done so a long-running multi-tenant process
  with user churn can't grow it without limit (clears on overflow)
- purge route: distinct 400 for a missing doc_types key; enforce the admin
  check even for an empty no-op request (destructive route)
- tests: missing-key 400, admin-gated empty no-op, non-admin empty 403

The _consent_narrowed_doc_types precondition is enforced by its non-Optional
frozenset[str] signature (ty rejects a None caller). The httpx.BasicAuth
SonarCloud hotspot matches the existing webhook routes (false positive,
credential from the app-password store) — left consistent for UI triage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-16 01:21:01 +02:00
co-authored by Claude Opus 4.8
parent 24b8000a71
commit cef477b877
4 changed files with 64 additions and 11 deletions
+11 -3
View File
@@ -77,6 +77,11 @@ async def purge_doc_types_route(request: Request) -> JSONResponse:
)
raw = body.get("doc_types")
if raw is None:
return JSONResponse(
{"error": "Bad request", "message": "doc_types is required"},
status_code=400,
)
if not isinstance(raw, list) or not all(isinstance(d, str) for d in raw):
return JSONResponse(
{
@@ -86,8 +91,6 @@ async def purge_doc_types_route(request: Request) -> JSONResponse:
status_code=400,
)
doc_types = [d for d in raw if d]
if not doc_types:
return JSONResponse({"purged": {}})
# Bound the batch: there are only a handful of real indexed types, so a huge
# list is abuse — cap it rather than fan out unbounded count+delete calls.
if len(doc_types) > _MAX_PURGE_DOC_TYPES:
@@ -107,7 +110,9 @@ async def purge_doc_types_route(request: Request) -> JSONResponse:
if not nextcloud_host:
raise ValueError("Nextcloud host not configured")
# Verify admin via the caller's own app password before any deletion.
# Verify admin via the caller's own app password before any deletion
# enforced even for an empty (no-op) request, since this is a
# destructive admin route.
async with nextcloud_httpx_client(
base_url=nextcloud_host,
auth=httpx.BasicAuth(username, app_password),
@@ -125,6 +130,9 @@ async def purge_doc_types_route(request: Request) -> JSONResponse:
status_code=403,
)
if not doc_types:
return JSONResponse({"purged": {}})
purged = await purge_doc_types(doc_types)
logger.info("Vector-sync purge by admin %s: %s", user_id, purged)
return JSONResponse({"purged": purged})