diff --git a/docs/ADR-002-vector-sync-authentication.md b/docs/ADR-002-vector-sync-authentication.md index aee8708..42b5b2f 100644 --- a/docs/ADR-002-vector-sync-authentication.md +++ b/docs/ADR-002-vector-sync-authentication.md @@ -51,14 +51,8 @@ We will implement a **tiered OAuth authentication strategy** for background oper - Background worker uses service account token directly - No user-specific delegation or impersonation - **Implementation**: `KeycloakOAuthClient.get_service_account_token()` (keycloak_oauth.py:341-395) -- **Testing**: - - ✅ **Automated test**: `tests/server/oauth/test_keycloak_external_idp.py::test_keycloak_service_account_token_acquisition` - - ✅ **Manual test**: `tests/manual/test_token_exchange.py` -- **Supported Providers**: - - ✅ **Keycloak** (external IdP mode) - Fully tested and validated - - ❌ **Nextcloud OIDC app** (integrated mode) - Not yet implemented (see app.py:631-635) - - The `KeycloakOAuthClient` class is provider-agnostic and works with any OIDC provider - - Extending support to Nextcloud OIDC app requires configuration/initialization only +- **Testing**: Manual test in `tests/manual/test_token_exchange.py` +- **TODO**: Automated integration tests needed for both Keycloak and Nextcloud OIDC app **Trade-offs**: - ✅ Works with nearly all OIDC providers diff --git a/tests/conftest.py b/tests/conftest.py index cd94c7e..880fe98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2526,59 +2526,6 @@ async def keycloak_oauth_client_credentials(anyio_backend, oauth_callback_server # No cleanup needed - client is pre-configured in realm export -@pytest.fixture(scope="session") -async def keycloak_oauth_client(anyio_backend, keycloak_oauth_client_credentials): - """ - Fixture to create a KeycloakOAuthClient instance for service account token operations. - - This fixture is used to test ADR-002 Tier 1 (service account token acquisition) and - Tier 3 (token exchange with delegation). - - Returns: - KeycloakOAuthClient instance configured with Keycloak credentials - """ - from nextcloud_mcp_server.auth.keycloak_oauth import KeycloakOAuthClient - - # Get Keycloak configuration from environment - keycloak_discovery_url = os.getenv( - "OIDC_DISCOVERY_URL", - "http://localhost:8888/realms/nextcloud-mcp/.well-known/openid-configuration", - ) - - # Extract base URL and realm from discovery URL - # Format: http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration - if "/realms/" in keycloak_discovery_url: - base_url = keycloak_discovery_url.split("/realms/")[0] - realm = keycloak_discovery_url.split("/realms/")[1].split("/")[0] - else: - pytest.skip("Invalid Keycloak discovery URL format") - - client_id, client_secret, callback_url, _, _ = keycloak_oauth_client_credentials - - logger.info("Creating KeycloakOAuthClient for service account operations...") - logger.info(f" Keycloak URL: {base_url}") - logger.info(f" Realm: {realm}") - logger.info(f" Client ID: {client_id}") - - oauth_client = KeycloakOAuthClient( - keycloak_url=base_url, - realm=realm, - client_id=client_id, - client_secret=client_secret, - redirect_uri=callback_url, - ) - - # Discover endpoints - await oauth_client.discover() - logger.info("✓ KeycloakOAuthClient initialized") - logger.info(f" Token endpoint: {oauth_client.token_endpoint}") - - yield oauth_client - - # Cleanup (close http client if needed) - await oauth_client.close() - - async def _get_keycloak_oauth_token( browser, keycloak_oauth_client_credentials, diff --git a/tests/server/oauth/test_keycloak_external_idp.py b/tests/server/oauth/test_keycloak_external_idp.py index 7fb7f2d..99da439 100644 --- a/tests/server/oauth/test_keycloak_external_idp.py +++ b/tests/server/oauth/test_keycloak_external_idp.py @@ -20,7 +20,6 @@ Tests: import json import logging -import os import pytest @@ -93,80 +92,6 @@ async def test_keycloak_oauth_client_credentials_discovery( logger.info(f" Authorization endpoint: {authorization_endpoint}") -async def test_keycloak_service_account_token_acquisition(keycloak_oauth_client): - """Test service account token acquisition via client_credentials grant (ADR-002 Tier 1). - - Verifies: - - Service account token is acquired using client_credentials grant - - Token response includes access_token, token_type, expires_in - - Token can be used to access Nextcloud APIs - - Token type is Bearer - - This test validates ADR-002 Tier 1 implementation for Keycloak external IdP. - - Note: For Nextcloud OIDC app (integrated mode), service account token acquisition - is not yet implemented. See app.py:631-635 which states "OAuth client for token - refresh not yet implemented for integrated mode". The KeycloakOAuthClient class - works with any OIDC provider, so extending support to Nextcloud OIDC app is - primarily a configuration/initialization issue rather than a fundamental limitation. - """ - # Get service account token with standard scopes - token_response = await keycloak_oauth_client.get_service_account_token( - scopes=["openid", "profile", "email"] - ) - - # Verify token response structure - assert "access_token" in token_response, "Missing access_token in response" - assert "token_type" in token_response, "Missing token_type in response" - assert "expires_in" in token_response, "Missing expires_in in response" - - assert token_response["token_type"].lower() == "bearer", ( - f"Expected Bearer token type, got {token_response['token_type']}" - ) - assert isinstance(token_response["expires_in"], int), ( - f"Expected integer expires_in, got {type(token_response['expires_in'])}" - ) - assert token_response["expires_in"] > 0, ( - f"Expected positive expires_in, got {token_response['expires_in']}" - ) - - logger.info("✓ Service account token acquired successfully") - logger.info(f" Token type: {token_response['token_type']}") - logger.info(f" Expires in: {token_response['expires_in']}s") - logger.info(f" Scope: {token_response.get('scope', 'N/A')}") - logger.info(f" Token length: {len(token_response['access_token'])} chars") - - # Verify token works with Nextcloud APIs - # The service account token should be validated by Nextcloud's user_oidc app - from nextcloud_mcp_server.client import NextcloudClient - - nextcloud_host = os.getenv("NEXTCLOUD_HOST", "http://localhost:8080") - - # Create a NextcloudClient using the service account token - nc_client = NextcloudClient.from_token( - base_url=nextcloud_host, - token=token_response["access_token"], - username="service-account-nextcloud-mcp-server", # Keycloak service account username - ) - - try: - # Verify token works with Nextcloud API (using OCS endpoint which works without patch) - capabilities = await nc_client.capabilities() - assert capabilities is not None, ( - "Failed to get capabilities with service account token" - ) - - logger.info("✓ Service account token works with Nextcloud APIs") - logger.info( - f" Nextcloud version: {capabilities.get('version', {}).get('string', 'unknown')}" - ) - - finally: - await nc_client.close() - - logger.info("✓ ADR-002 Tier 1 (Service Account Token) validated for Keycloak") - - # ============================================================================ # MCP Server Connectivity Tests # ============================================================================