The webhook API endpoints in api/webhooks.py forwarded the inbound MCP OAuth bearer token directly to Nextcloud as the Authorization header. Per ADR-022 / docs/login-flow-v2.md the data leg from MCP server to Nextcloud must use HTTP Basic Auth with the user's stored Login Flow v2 app password — bearer-forwarding requires upstream user_oidc patches that were never merged and is incompatible with admin endpoints gated by @PasswordConfirmationRequired (e.g. webhook_listeners/api/v1/webhooks, which 401s). PR #760 papered over the symptom for /api/v1/apps by switching to the permissive /cloud/capabilities endpoint, but the same architectural mistake remained on list_webhooks / create_webhook / delete_webhook, which still 500'd on the astrolabe admin UI's preset page. Changes: - New helper api/_auth.py:get_basic_auth_for_user(user_id) reads the user's app password from encrypted storage and returns (username, app_password). Mirrors context.py:_get_client_from_login_flow but is callable from Starlette routes (no MCP Context required). - All four endpoints in api/webhooks.py now use httpx.BasicAuth instead of forwarding the OAuth bearer; ProvisioningRequiredError is mapped to HTTP 412 so callers can render a "complete provisioning" CTA rather than receiving an opaque 500. - Outbound NC requests now identify the user by the username recorded at Login Flow v2 provisioning time (which may differ from the IdP-issued user_id) — flowed into WebhooksClient and used for logging. Tests: - tests/unit/test_management_apps_endpoint.py: assertions updated to verify outbound NC request uses BasicAuth and carries no Authorization header. Replaced "missing-Authorization → 500" test with a ProvisioningRequiredError → 412 case. - tests/unit/test_webhooks_api_auth.py (new): cross-endpoint coverage for list_webhooks, create_webhook, delete_webhook and the new helper — including 412 symmetry for all four endpoints. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
49 lines
2.1 KiB
Python
49 lines
2.1 KiB
Python
"""Credential resolution helpers for non-MCP-Context API endpoints.
|
|
|
|
Starlette routes (e.g. ``api/webhooks.py``) authenticate the inbound request
|
|
via OAuth bearer validation, then need to make Nextcloud API calls on behalf
|
|
of that user. Per ADR-022 / ``docs/login-flow-v2.md`` the data leg to
|
|
Nextcloud always uses **HTTP Basic Auth with the user's app password**, never
|
|
the OAuth token. This module resolves that credential pair.
|
|
|
|
The MCP tool path uses ``context.get_client(ctx)``; this is the equivalent
|
|
helper for callers that have a validated ``user_id`` but no MCP ``Context``.
|
|
"""
|
|
|
|
from nextcloud_mcp_server.auth.scope_authorization import ProvisioningRequiredError
|
|
from nextcloud_mcp_server.auth.storage import get_shared_storage
|
|
|
|
|
|
async def get_basic_auth_for_user(user_id: str) -> tuple[str, str]:
|
|
"""Resolve ``(username, app_password)`` for an OAuth-validated user.
|
|
|
|
Reads the per-user app password provisioned via Login Flow v2 from
|
|
encrypted SQLite storage. The username returned is the actual Nextcloud
|
|
username recorded at provisioning time (which may differ from the IdP
|
|
user-id when an external IdP is configured).
|
|
|
|
Args:
|
|
user_id: MCP user identifier extracted from a validated OAuth token.
|
|
|
|
Returns:
|
|
Tuple ``(username, app_password)`` ready to pass to
|
|
``httpx.BasicAuth``.
|
|
|
|
Raises:
|
|
ProvisioningRequiredError: No app password is stored for ``user_id``.
|
|
The caller should surface this to the client so the user can
|
|
complete Login Flow v2 (typically via ``nc_auth_provision_access``
|
|
or the Astrolabe settings page).
|
|
"""
|
|
storage = await get_shared_storage()
|
|
app_data = await storage.get_app_password_with_scopes(user_id)
|
|
if not app_data:
|
|
raise ProvisioningRequiredError(
|
|
f"No Nextcloud app password provisioned for user {user_id!r}. "
|
|
"Complete Login Flow v2 (nc_auth_provision_access) before "
|
|
"calling this endpoint."
|
|
)
|
|
|
|
username = app_data.get("username") or user_id
|
|
return username, app_data["app_password"]
|