fix(api): use stored app password for chunk-context and pdf-preview
The /api/v1/chunk-context and /api/v1/pdf-preview handlers in api/visualization.py forwarded the incoming OAuth bearer directly to Nextcloud via NextcloudClient.from_token. In multi-user BasicAuth mode Nextcloud has no validator for those bearers on Notes/WebDAV, so it treats the request as anonymous and returns 401 — surfaced to the user as a 500 from /apps/astrolabe/api/chunk-context. Search worked because it only hits Qdrant. Architecturally, OAuth is only for Astrolabe→MCP server; MCP server→ Nextcloud always uses the per-user app password stored during provision (background sync already does this via vector.oauth_sync). - Resolve the Nextcloud client through get_user_client_basic_auth in both get_chunk_context and get_pdf_preview, surfacing NotProvisionedError as a clean 401 instead of opaque 500. - Apply the same fix to the session-cookie variant in auth/viz_routes.chunk_context_endpoint for the internal viz UI. Tests: - New unit file test_management_chunk_context_endpoint.py, including a regression guard that asserts get_user_client_basic_auth is awaited (so reverting to from_token fails without needing a live Nextcloud). - Updated test_management_pdf_preview_endpoint.py to mock the new auth path (drops extract_bearer_token / NextcloudClient.from_token patches). - New integration test test_astrolabe_chunk_context.py drives the full chain (browser → Astrolabe → MCP → Nextcloud) in multi-user BasicAuth mode, plus bare-bones 401 checks on the MCP endpoint. Full unit suite: 546 passed. Companion PR on astrolabe (cbcoutinho/astrolabe#66) sends the Nextcloud UID as loginName in the app-password POST body so the stored record is complete. Submodule bump to that branch will follow once CI reproduces the failure on the old submodule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d766f3c014
commit
8a0d06107e
@@ -34,6 +34,10 @@ from nextcloud_mcp_server.search import (
|
||||
SemanticSearchAlgorithm,
|
||||
)
|
||||
from nextcloud_mcp_server.search.context import get_chunk_with_context
|
||||
from nextcloud_mcp_server.vector.oauth_sync import (
|
||||
NotProvisionedError,
|
||||
get_user_client_basic_auth,
|
||||
)
|
||||
from nextcloud_mcp_server.vector.pca import PCA
|
||||
from nextcloud_mcp_server.vector.placeholder import get_placeholder_filter
|
||||
from nextcloud_mcp_server.vector.qdrant_client import get_qdrant_client
|
||||
@@ -554,12 +558,28 @@ async def chunk_context_endpoint(request: Request) -> JSONResponse:
|
||||
# Convert doc_id to int (all document types use int IDs)
|
||||
doc_id_int = int(doc_id)
|
||||
|
||||
# Get authenticated Nextcloud client
|
||||
# Use context expansion module to fetch chunk with surrounding context
|
||||
async with await _get_authenticated_client_for_userinfo(request) as nc_client:
|
||||
user_id = request.user.display_name
|
||||
settings = get_settings()
|
||||
nextcloud_host = settings.nextcloud_host
|
||||
if not nextcloud_host:
|
||||
raise RuntimeError("Nextcloud host not configured")
|
||||
|
||||
# Use the user's stored app password for Nextcloud calls.
|
||||
# The session cookie only authenticates the browser → MCP Server hop;
|
||||
# MCP Server → Nextcloud always uses the app password provisioned
|
||||
# during the authorization step.
|
||||
try:
|
||||
nc_client = await get_user_client_basic_auth(user_id, nextcloud_host)
|
||||
except NotProvisionedError as e:
|
||||
return JSONResponse(
|
||||
{"success": False, "error": str(e)},
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
async with nc_client:
|
||||
chunk_context = await get_chunk_with_context(
|
||||
nc_client=nc_client,
|
||||
user_id=request.user.display_name, # User ID from auth
|
||||
user_id=user_id,
|
||||
doc_id=doc_id_int,
|
||||
doc_type=doc_type,
|
||||
chunk_start=start,
|
||||
|
||||
Reference in New Issue
Block a user