fix(webhooks): use OCS v2 capabilities for /api/v1/apps
The Astrolabe webhooks UI hits /api/v1/apps on the MCP server, which forwarded the OAuth bearer token to /ocs/v1.php/cloud/apps?filter=enabled. That OCS endpoint is admin-only AND @PasswordConfirmationRequired — neither requirement is satisfiable via an OAuth bearer token, so even an admin user's token returns a silent 401 (no entry in nextcloud.log). Switch to /ocs/v2.php/cloud/capabilities, which has no admin or password- confirmation gate, accepts the existing bearer token, and returns a capabilities map keyed by app id (notes, files, tables, forms, etc.). This is sufficient for the webhook presets UI to gate available presets against the running Nextcloud instance's enabled apps. Bearer is preserved on the outbound call because anonymous capabilities omits notes/tables/forms — only authenticated capabilities exposes them. Tests: - New unit test covers the regression (asserts /ocs/v2.php/cloud/capabilities is hit, NOT /cloud/apps), response parsing, sanitized error messages, and missing-config paths. - New integration test under tests/server/login_flow/ drives a real OAuth flow against mcp-login-flow with a static OIDC client (nextcloudMcpServerUIPublicClient) and asserts /api/v1/apps returns 200 with core/files in the response. docker-compose.yml: aligns mcp-login-flow's ALLOWED_MGMT_CLIENT with mcp-multi-user-basic so the same static-client test fixture works for both. Follow-up to homelab-argocd #1608, which set ALLOWED_MGMT_CLIENT in production but didn't unblock the webhooks flow. 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
e2b9258dec
commit
148ab8c117
@@ -47,7 +47,9 @@ async def get_installed_apps(request: Request) -> JSONResponse:
|
||||
)
|
||||
|
||||
try:
|
||||
# Get Bearer token from request
|
||||
# Get Bearer token from request — forwarded to Nextcloud so the
|
||||
# capabilities response includes per-user / per-app entries (anonymous
|
||||
# capabilities omits notes, tables, forms etc.).
|
||||
token = extract_bearer_token(request)
|
||||
if not token:
|
||||
raise ValueError("Missing Authorization header")
|
||||
@@ -59,21 +61,20 @@ async def get_installed_apps(request: Request) -> JSONResponse:
|
||||
if not nextcloud_host:
|
||||
raise ValueError("Nextcloud host not configured")
|
||||
|
||||
# Create authenticated HTTP client
|
||||
# Use OCS v2 capabilities. The legacy /ocs/v1.php/cloud/apps endpoint is
|
||||
# admin-only AND @PasswordConfirmationRequired — neither is satisfiable
|
||||
# via an OAuth bearer token, so it always 401s. Capabilities has no such
|
||||
# gates and returns a map keyed by app id for every enabled app that
|
||||
# implements OCSCapabilities, which is sufficient to populate the
|
||||
# webhook presets UI.
|
||||
async with nextcloud_httpx_client(
|
||||
base_url=nextcloud_host,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=30.0,
|
||||
) as client:
|
||||
# Get installed apps using OCS API
|
||||
# Notes, Calendar, Deck, Tables, etc. are apps that support webhooks
|
||||
# We check which ones are installed and enabled
|
||||
ocs_url = "/ocs/v1.php/cloud/apps"
|
||||
params = {"filter": "enabled"}
|
||||
|
||||
response = await client.get(
|
||||
ocs_url,
|
||||
params=params,
|
||||
"/ocs/v2.php/cloud/capabilities",
|
||||
params={"format": "json"},
|
||||
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
||||
)
|
||||
|
||||
@@ -81,7 +82,8 @@ async def get_installed_apps(request: Request) -> JSONResponse:
|
||||
raise ValueError(f"OCS API returned status {response.status_code}")
|
||||
|
||||
data = response.json()
|
||||
apps = data.get("ocs", {}).get("data", {}).get("apps", [])
|
||||
capabilities = data.get("ocs", {}).get("data", {}).get("capabilities", {})
|
||||
apps = sorted(capabilities.keys())
|
||||
|
||||
return JSONResponse({"apps": apps})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user