34df5f5b9a
This commit implements and documents both RFC 8693 token exchange tiers from ADR-002, enabling both production-ready delegation and advanced impersonation capabilities. - Enable Keycloak preview features (`--features=preview`) to support both Standard V2 and Legacy V1 token exchange modes - Update Tier 1 status from "NOT IMPLEMENTED" to "IMPLEMENTED (Legacy V1)" - Add detailed empirical testing results showing: - Standard V2 rejects `requested_subject` parameter - Legacy V1 accepts parameter but requires impersonation permissions - Complete configuration steps for enabling impersonation - Add comparison table showing when to use each tier - Add "When to Use" guidance for both tiers - Document that Tier 2 (Delegation) is the recommended default - Update docstring to document both Tier 1 and Tier 2 support - Add tier-specific logging (shows which tier is being used) - Document permission requirements for Tier 1 impersonation **tests/integration/auth/test_token_exchange_standard_v2.py**: - Test delegation without impersonation (Tier 2) - Verify sub claim remains unchanged (service account identity) - Verify no special permissions required - Test exchanged tokens work with Nextcloud APIs - All tests PASS ✅ **tests/integration/auth/test_token_exchange_legacy_v1.py**: - Test impersonation with `requested_subject` (Tier 1) - Verify sub claim changes to target user - Auto-skip if impersonation permissions not configured - Document permission requirements in test docstrings - Test exchanged tokens work with Nextcloud APIs **tests/manual/test_impersonation.py**: - Comprehensive impersonation validation script - Tests both Standard V2 and Legacy V1 behavior - Decodes JWT tokens to verify sub claim changes - Validates tokens against Nextcloud APIs **tests/manual/configure_impersonation.py**: - Automated permission configuration helper - Documents manual Keycloak CLI configuration steps Both token exchange tiers are now fully implemented and tested: - **Tier 2 (Delegation)** - ✅ RECOMMENDED - Standard V2 (production-ready) - No special permissions required - Service account identity preserved - **Tier 1 (Impersonation)** - ✅ Advanced use only - Legacy V1 (--features=preview required) - Requires manual permission grant via Keycloak CLI - Subject claim changes to target user 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
178 lines
6.9 KiB
YAML
178 lines
6.9 KiB
YAML
services:
|
|
# Note: MariaDB is external service. You can find more information about the configuration here:
|
|
# https://hub.docker.com/_/mariadb
|
|
db:
|
|
# Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
|
|
image: docker.io/library/mariadb:lts@sha256:ae6119716edac6998ae85508431b3d2e666530ddf4e94c61a10710caec9b0f71
|
|
restart: always
|
|
command: --transaction-isolation=READ-COMMITTED
|
|
volumes:
|
|
- db:/var/lib/mysql
|
|
environment:
|
|
- MYSQL_ROOT_PASSWORD=password
|
|
- MYSQL_PASSWORD=password
|
|
- MYSQL_DATABASE=nextcloud
|
|
- MYSQL_USER=nextcloud
|
|
|
|
# Note: Redis is an external service. You can find more information about the configuration here:
|
|
# https://hub.docker.com/_/redis
|
|
redis:
|
|
image: docker.io/library/redis:alpine@sha256:59b6e694653476de2c992937ebe1c64182af4728e54bb49e9b7a6c26614d8933
|
|
restart: always
|
|
|
|
app:
|
|
image: docker.io/library/nextcloud:32.0.1@sha256:1e4eae55eebe094cae6f9e7b6e0b4bccf4a4fe7b7e6f6f8f57010994b3b2ee42
|
|
restart: always
|
|
ports:
|
|
- 0.0.0.0:8080:80
|
|
depends_on:
|
|
- redis
|
|
- db
|
|
volumes:
|
|
- nextcloud:/var/www/html
|
|
- ./app-hooks:/docker-entrypoint-hooks.d:ro
|
|
# Mount OIDC development directory outside /var/www/html to avoid rsync conflicts
|
|
# The post-installation hook will register /opt/apps as an additional app directory
|
|
- ./third_party:/opt/apps:ro
|
|
environment:
|
|
- NEXTCLOUD_TRUSTED_DOMAINS=app
|
|
- NEXTCLOUD_ADMIN_USER=admin
|
|
- NEXTCLOUD_ADMIN_PASSWORD=admin
|
|
- MYSQL_PASSWORD=password
|
|
- MYSQL_DATABASE=nextcloud
|
|
- MYSQL_USER=nextcloud
|
|
- MYSQL_HOST=db
|
|
- REDIS_HOST=redis
|
|
#healthcheck:
|
|
#test: ["CMD-SHELL", "curl -Ss http://localhost/status.php | grep '\"installed\":true' || exit 1"]
|
|
#interval: 10s
|
|
#timeout: 30s
|
|
#retries: 30
|
|
|
|
recipes:
|
|
image: docker.io/library/nginx:alpine@sha256:b3c656d55d7ad751196f21b7fd2e8d4da9cb430e32f646adcf92441b72f82b14
|
|
restart: always
|
|
volumes:
|
|
- ./tests/fixtures/test_recipe.html:/usr/share/nginx/html/test_recipe.html:ro
|
|
- ./tests/fixtures/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
|
|
unstructured:
|
|
image: downloads.unstructured.io/unstructured-io/unstructured-api:latest@sha256:a43ab55898599157fb0e0e097dabb8ecdd1d8e3df1ae5b67c6e15a136b171a6c
|
|
restart: always
|
|
ports:
|
|
- 127.0.0.1:8002:8000
|
|
# Unstructured API runs on port 8000 internally
|
|
# We expose it on 8002 externally to avoid conflict
|
|
profiles:
|
|
- unstructured
|
|
|
|
mcp:
|
|
build: .
|
|
command: ["--transport", "streamable-http"]
|
|
restart: always
|
|
depends_on:
|
|
- app
|
|
ports:
|
|
- 127.0.0.1:8000:8000
|
|
environment:
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_USERNAME=admin
|
|
- NEXTCLOUD_PASSWORD=admin
|
|
|
|
mcp-oauth:
|
|
build: .
|
|
command: ["--transport", "streamable-http", "--oauth", "--port", "8001", "--oauth-token-type", "jwt"]
|
|
restart: always
|
|
depends_on:
|
|
- app
|
|
ports:
|
|
- 127.0.0.1:8001:8001
|
|
environment:
|
|
# Generic OIDC configuration (integrated mode - Nextcloud OIDC app)
|
|
# OIDC_DISCOVERY_URL not set - defaults to NEXTCLOUD_HOST/.well-known/openid-configuration
|
|
# OIDC_CLIENT_ID not set - uses Dynamic Client Registration (DCR)
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8001
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
|
- NEXTCLOUD_OIDC_SCOPES=openid profile email notes:read notes:write calendar:read calendar:write contacts:read contacts:write cookbook:read cookbook:write deck:read deck:write tables:read tables:write files:read files:write sharing:read sharing:write todo:read todo:write
|
|
|
|
# Refresh token storage (ADR-002 Tier 1)
|
|
- ENABLE_OFFLINE_ACCESS=true
|
|
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
|
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
|
|
|
# NO admin credentials - using OAuth with Dynamic Client Registration (DCR)
|
|
# Client credentials registered via RFC 7591 and stored in volume
|
|
# JWT token type is used for testing (faster validation, scopes embedded in token)
|
|
volumes:
|
|
- oauth-client-storage:/app/.oauth
|
|
- oauth-tokens:/app/data
|
|
|
|
keycloak:
|
|
image: quay.io/keycloak/keycloak:26.4.2
|
|
command:
|
|
- "start-dev"
|
|
- "--import-realm"
|
|
- "--hostname=http://localhost:8888"
|
|
- "--hostname-strict=false"
|
|
- "--hostname-backchannel-dynamic=true"
|
|
- "--features=preview" # Enable Legacy V1 token exchange (supports both Standard V2 and Legacy V1)
|
|
ports:
|
|
- 127.0.0.1:8888:8080
|
|
environment:
|
|
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
|
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
|
|
volumes:
|
|
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /realms/nextcloud-mcp HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q 'HTTP/1.1 200'"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 30
|
|
|
|
mcp-keycloak:
|
|
build: .
|
|
command: ["--transport", "streamable-http", "--oauth", "--port", "8002"]
|
|
restart: always
|
|
depends_on:
|
|
keycloak:
|
|
condition: service_healthy
|
|
app:
|
|
condition: service_started
|
|
ports:
|
|
- 127.0.0.1:8002:8002
|
|
environment:
|
|
# Generic OIDC configuration (external IdP mode - Keycloak)
|
|
# Provider auto-detected from OIDC_DISCOVERY_URL issuer
|
|
# Using internal Docker hostname for discovery to get consistent issuer
|
|
- OIDC_DISCOVERY_URL=http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
|
|
- OIDC_CLIENT_ID=nextcloud-mcp-server
|
|
- OIDC_CLIENT_SECRET=mcp-secret-change-in-production
|
|
- OIDC_JWKS_URI=http://keycloak:8080/realms/nextcloud-mcp/protocol/openid-connect/certs
|
|
|
|
# Nextcloud API endpoint (for accessing APIs with validated token)
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8888/realms/nextcloud-mcp
|
|
|
|
# Refresh token storage (ADR-002 Tier 1 & 2)
|
|
- ENABLE_OFFLINE_ACCESS=true
|
|
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
|
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
|
|
|
# OAuth scopes (optional - uses defaults if not specified)
|
|
- NEXTCLOUD_OIDC_SCOPES=openid profile email offline_access notes:read notes:write calendar:read calendar:write contacts:read contacts:write cookbook:read cookbook:write deck:read deck:write tables:read tables:write files:read files:write sharing:read sharing:write todo:read todo:write
|
|
|
|
# NO admin credentials - using external IdP OAuth only!
|
|
volumes:
|
|
- keycloak-tokens:/app/data
|
|
- keycloak-oauth-storage:/app/.oauth
|
|
|
|
volumes:
|
|
nextcloud:
|
|
db:
|
|
oauth-client-storage:
|
|
oauth-tokens:
|
|
keycloak-tokens:
|
|
keycloak-oauth-storage:
|