refactor(config)!: derive enable_login_flow from mode, remove ENABLE_LOGIN_FLOW env var
Once OAUTH_SINGLE_AUDIENCE was renamed to LOGIN_FLOW and the validation
gate ensured the only meaningful configuration was
`MCP_DEPLOYMENT_MODE=login_flow + ENABLE_LOGIN_FLOW=true`, the two
controls became redundant. Setting the mode is sufficient; the
ENABLE_LOGIN_FLOW env var doesn't add information.
This commit makes the deployment mode the single source of truth for
the Login Flow v2 toggle:
- `nextcloud_mcp_server/config.py`: drop the `ENABLE_LOGIN_FLOW`
dynaconf env-var alias. The `enable_login_flow` field stays as an
internal attribute so the 6 runtime call sites (app.py x4,
context.py, auth/scope_authorization.py) keep working unchanged.
Updated field docstring to flag it as derived.
- `nextcloud_mcp_server/config_validators.py`:
- Drop `enable_login_flow` from `MODE_REQUIREMENTS[LOGIN_FLOW].required`.
- Drop the validation gate that required ENABLE_LOGIN_FLOW=true for
LOGIN_FLOW mode (no longer possible to misconfigure — the flag is
derived, not user input).
- Add `_sync_derived_flags()` helper called at every return path of
`detect_auth_mode` to set `settings.enable_login_flow` from the
resolved mode.
- `tests/unit/test_config_validators.py`: drop `enable_login_flow=True`
from happy-path fixtures (no longer needed — detection sets it).
Repurpose `test_login_flow_requires_enable_login_flow_flag` into
`test_login_flow_mode_auto_derives_enable_login_flow_flag` which
asserts the new auto-derivation behaviour for both LOGIN_FLOW and a
non-LOGIN_FLOW mode.
- `docker-compose.yml`: remove `ENABLE_LOGIN_FLOW=true` from the
`mcp-login-flow` and `mcp-keycloak` profiles.
- `env.sample`: remove the ENABLE_LOGIN_FLOW reference; the comment
on `MCP_DEPLOYMENT_MODE` now notes the derived flag.
- `docs/configuration.md`, `docs/authentication.md`,
`docs/login-flow-v2.md`, `docs/auth-flows.md`,
`docs/troubleshooting.md`, `docs/ADR-025-*.md`: replace
ENABLE_LOGIN_FLOW=true examples and references with
MCP_DEPLOYMENT_MODE=login_flow.
BREAKING CHANGE: `ENABLE_LOGIN_FLOW` is no longer read from the
environment. Anyone who relied on `ENABLE_LOGIN_FLOW=true` to activate
Login Flow v2 should set `MCP_DEPLOYMENT_MODE=login_flow` instead (or
rely on it being the default when no other auth env vars are set).
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
c74ef014ee
commit
df4994e860
+4
-5
@@ -241,8 +241,8 @@ services:
|
|||||||
- ENABLE_TOKEN_EXCHANGE=true
|
- ENABLE_TOKEN_EXCHANGE=true
|
||||||
- TOKEN_EXCHANGE_CACHE_TTL=300 # Cache exchanged tokens for 5 minutes (default)
|
- TOKEN_EXCHANGE_CACHE_TTL=300 # Cache exchanged tokens for 5 minutes (default)
|
||||||
|
|
||||||
# Login Flow v2 (ADR-022) with external IdP
|
# Login Flow v2 (ADR-022) with external IdP — derived from the
|
||||||
- ENABLE_LOGIN_FLOW=true
|
# auto-detected LOGIN_FLOW deployment mode; no separate flag needed.
|
||||||
- ENABLE_DCR=true
|
- ENABLE_DCR=true
|
||||||
|
|
||||||
# OAuth scopes (optional - uses defaults if not specified)
|
# OAuth scopes (optional - uses defaults if not specified)
|
||||||
@@ -275,10 +275,9 @@ services:
|
|||||||
#- NEXTCLOUD_MCP_SERVER_URL=https://nextcloud-mcp-dev.tail148d5.ts.net
|
#- NEXTCLOUD_MCP_SERVER_URL=https://nextcloud-mcp-dev.tail148d5.ts.net
|
||||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||||
|
|
||||||
# Login Flow v2 (ADR-022) — explicit mode + the flag that enables the
|
# Login Flow v2 (ADR-022) — the deployment mode is the single switch;
|
||||||
# browser-based app-password acquisition layer
|
# the browser-based app-password layer is derived automatically.
|
||||||
- MCP_DEPLOYMENT_MODE=login_flow
|
- MCP_DEPLOYMENT_MODE=login_flow
|
||||||
- ENABLE_LOGIN_FLOW=true
|
|
||||||
|
|
||||||
# Token storage (required for app password + session persistence).
|
# Token storage (required for app password + session persistence).
|
||||||
# Source the key from .env — see env.sample. To generate a fresh key:
|
# Source the key from .env — see env.sample. To generate a fresh key:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ The nextcloud-mcp-server configuration system has grown to ~80+ environment vari
|
|||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| Core Nextcloud | 6 | `NEXTCLOUD_HOST`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_VERIFY_SSL` |
|
| Core Nextcloud | 6 | `NEXTCLOUD_HOST`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_VERIFY_SSL` |
|
||||||
| OAuth/OIDC | 12 | `OIDC_DISCOVERY_URL`, `NEXTCLOUD_OIDC_CLIENT_ID`, `JWKS_URI` |
|
| OAuth/OIDC | 12 | `OIDC_DISCOVERY_URL`, `NEXTCLOUD_OIDC_CLIENT_ID`, `JWKS_URI` |
|
||||||
| Mode Selection | 4 | `MCP_DEPLOYMENT_MODE`, `ENABLE_LOGIN_FLOW`, `ENABLE_TOKEN_EXCHANGE` |
|
| Mode Selection | 2 | `MCP_DEPLOYMENT_MODE`, `ENABLE_MULTI_USER_BASIC_AUTH` |
|
||||||
| Token Storage | 3 | `TOKEN_ENCRYPTION_KEY`, `TOKEN_STORAGE_DB` |
|
| Token Storage | 3 | `TOKEN_ENCRYPTION_KEY`, `TOKEN_STORAGE_DB` |
|
||||||
| Semantic Search | 6 | `ENABLE_SEMANTIC_SEARCH`, `VECTOR_SYNC_SCAN_INTERVAL` |
|
| Semantic Search | 6 | `ENABLE_SEMANTIC_SEARCH`, `VECTOR_SYNC_SCAN_INTERVAL` |
|
||||||
| Qdrant | 4 | `QDRANT_URL`, `QDRANT_LOCATION`, `QDRANT_API_KEY` |
|
| Qdrant | 4 | `QDRANT_URL`, `QDRANT_LOCATION`, `QDRANT_API_KEY` |
|
||||||
@@ -115,7 +115,8 @@ nextcloud_ca_bundle = "@none"
|
|||||||
|
|
||||||
# === Authentication Toggles ===
|
# === Authentication Toggles ===
|
||||||
enable_multi_user_basic_auth = false
|
enable_multi_user_basic_auth = false
|
||||||
enable_login_flow = false
|
# `enable_login_flow` is derived from MCP_DEPLOYMENT_MODE=login_flow in
|
||||||
|
# detect_auth_mode (ADR-022 follow-up) — no separate toggle.
|
||||||
enable_token_exchange = false
|
enable_token_exchange = false
|
||||||
|
|
||||||
# === Token Storage ===
|
# === Token Storage ===
|
||||||
@@ -202,7 +203,7 @@ enable_multi_user_basic_auth = true
|
|||||||
token_storage_db = "/app/data/tokens.db"
|
token_storage_db = "/app/data/tokens.db"
|
||||||
|
|
||||||
[login_flow]
|
[login_flow]
|
||||||
enable_login_flow = true
|
# enable_login_flow is now derived from the mode (ADR-022 follow-up).
|
||||||
token_storage_db = "/app/data/tokens.db"
|
token_storage_db = "/app/data/tokens.db"
|
||||||
|
|
||||||
[keycloak]
|
[keycloak]
|
||||||
@@ -266,7 +267,7 @@ Dynaconf merges configuration in this order (last wins):
|
|||||||
|
|
||||||
This means:
|
This means:
|
||||||
- **File-based config is optional** — env vars alone still work (they override everything)
|
- **File-based config is optional** — env vars alone still work (they override everything)
|
||||||
- **Mode-specific defaults reduce boilerplate** — `[login_flow]` sets `enable_login_flow=true` and `token_storage_db=/app/data/tokens.db` so deployers don't need to
|
- **Mode-specific defaults reduce boilerplate** — `[login_flow]` sets `token_storage_db=/app/data/tokens.db` so deployers don't need to
|
||||||
- **Secrets are separated** — `.secrets.toml` holds `TOKEN_ENCRYPTION_KEY`, passwords, API keys
|
- **Secrets are separated** — `.secrets.toml` holds `TOKEN_ENCRYPTION_KEY`, passwords, API keys
|
||||||
- **Local dev overrides don't pollute** — `settings.local.toml` is gitignored
|
- **Local dev overrides don't pollute** — `settings.local.toml` is gitignored
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -231,7 +231,7 @@ TOKEN_STORAGE_DB=/app/data/tokens.db
|
|||||||
### Login Flow v2
|
### Login Flow v2
|
||||||
```bash
|
```bash
|
||||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||||
ENABLE_LOGIN_FLOW=true
|
MCP_DEPLOYMENT_MODE=login_flow
|
||||||
|
|
||||||
# Required for app-password storage
|
# Required for app-password storage
|
||||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ The server detects the active mode from environment variables at startup:
|
|||||||
|------------------|---------------|
|
|------------------|---------------|
|
||||||
| `NEXTCLOUD_USERNAME` + `NEXTCLOUD_PASSWORD` | Single-User (BasicAuth) |
|
| `NEXTCLOUD_USERNAME` + `NEXTCLOUD_PASSWORD` | Single-User (BasicAuth) |
|
||||||
| `ENABLE_MULTI_USER_BASIC_AUTH=true` (no creds) | Multi-User (BasicAuth pass-through) |
|
| `ENABLE_MULTI_USER_BASIC_AUTH=true` (no creds) | Multi-User (BasicAuth pass-through) |
|
||||||
| `ENABLE_LOGIN_FLOW=true` (no creds) | Multi-User (Login Flow v2) |
|
| `MCP_DEPLOYMENT_MODE=login_flow` or no auth env vars set | Multi-User (Login Flow v2) |
|
||||||
|
|
||||||
You can also force a mode via CLI flag:
|
You can also force a mode via CLI flag:
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ The recommended multi-user mode. MCP clients authenticate to the MCP server via
|
|||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||||
ENABLE_LOGIN_FLOW=true
|
MCP_DEPLOYMENT_MODE=login_flow
|
||||||
|
|
||||||
# App-password storage (required)
|
# App-password storage (required)
|
||||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||||
@@ -105,7 +105,7 @@ NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.instance.com
|
|||||||
| Variable | Required | Description |
|
| Variable | Required | Description |
|
||||||
|----------|----------|-------------|
|
|----------|----------|-------------|
|
||||||
| `NEXTCLOUD_HOST` | ✅ Yes | Internal URL of your Nextcloud instance (server-to-server) |
|
| `NEXTCLOUD_HOST` | ✅ Yes | Internal URL of your Nextcloud instance (server-to-server) |
|
||||||
| `ENABLE_LOGIN_FLOW` | ✅ Yes | Set to `true` to enable Login Flow v2 |
|
| `MCP_DEPLOYMENT_MODE` | ✅ Yes | Set to `login_flow` to select this mode. The Login Flow v2 browser-app-password layer is derived from the mode automatically — no separate flag needed. |
|
||||||
| `TOKEN_ENCRYPTION_KEY` | ✅ Yes | Fernet key for app-password encryption — generate with `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` |
|
| `TOKEN_ENCRYPTION_KEY` | ✅ Yes | Fernet key for app-password encryption — generate with `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` |
|
||||||
| `TOKEN_STORAGE_DB` | ✅ Yes | Path to SQLite DB for stored app passwords (use a persistent volume) |
|
| `TOKEN_STORAGE_DB` | ✅ Yes | Path to SQLite DB for stored app passwords (use a persistent volume) |
|
||||||
| `NEXTCLOUD_MCP_SERVER_URL` | ✅ Yes | Public URL of the MCP server (used as the audience claim and for browser redirects) |
|
| `NEXTCLOUD_MCP_SERVER_URL` | ✅ Yes | Public URL of the MCP server (used as the audience claim and for browser redirects) |
|
||||||
@@ -197,8 +197,7 @@ OLLAMA_BASE_URL=http://ollama:11434
|
|||||||
**Multi-User Login Flow v2 Mode:**
|
**Multi-User Login Flow v2 Mode:**
|
||||||
```dotenv
|
```dotenv
|
||||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||||
MCP_DEPLOYMENT_MODE=login_flow_v2
|
MCP_DEPLOYMENT_MODE=login_flow
|
||||||
ENABLE_LOGIN_FLOW=true
|
|
||||||
|
|
||||||
# Enable semantic search
|
# Enable semantic search
|
||||||
# In multi-user modes, this AUTOMATICALLY enables background operations!
|
# In multi-user modes, this AUTOMATICALLY enables background operations!
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ NEXTCLOUD_HOST=https://your.nextcloud.example.com
|
|||||||
NEXTCLOUD_OIDC_CLIENT_ID=<your-client-id>
|
NEXTCLOUD_OIDC_CLIENT_ID=<your-client-id>
|
||||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<your-client-secret>
|
NEXTCLOUD_OIDC_CLIENT_SECRET=<your-client-secret>
|
||||||
|
|
||||||
# Enable Login Flow v2 (per-user Nextcloud app-password provisioning for the data leg)
|
# Select Login Flow v2 mode (per-user Nextcloud app-password provisioning for the data leg)
|
||||||
ENABLE_LOGIN_FLOW=true
|
MCP_DEPLOYMENT_MODE=login_flow
|
||||||
|
|
||||||
# App-password storage (required for persistence across restarts)
|
# App-password storage (required for persistence across restarts)
|
||||||
TOKEN_STORAGE_DB=/app/data/tokens.db
|
TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||||
@@ -172,7 +172,7 @@ mcp-login-flow:
|
|||||||
- NEXTCLOUD_HOST=http://app:80
|
- NEXTCLOUD_HOST=http://app:80
|
||||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004
|
||||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||||
- ENABLE_LOGIN_FLOW=true
|
- MCP_DEPLOYMENT_MODE=login_flow
|
||||||
# Dev-only inline value. In production, mount via Docker secret and read
|
# Dev-only inline value. In production, mount via Docker secret and read
|
||||||
# from a *_FILE env var or a secrets-management init step.
|
# from a *_FILE env var or a secrets-management init step.
|
||||||
- TOKEN_ENCRYPTION_KEY=<your-fernet-key>
|
- TOKEN_ENCRYPTION_KEY=<your-fernet-key>
|
||||||
@@ -351,7 +351,7 @@ The user revoked it from **Settings → Security → Devices & Sessions**. Delet
|
|||||||
|
|
||||||
### Multiple worker processes
|
### Multiple worker processes
|
||||||
|
|
||||||
The provisioning session store is in-memory; `ENABLE_LOGIN_FLOW=true` assumes a single worker. Running with `uvicorn --workers N` will cause provisioning sessions to randomly fail. For higher concurrency, scale horizontally (multiple containers behind a sticky-session load balancer) rather than within a single process.
|
The provisioning session store is in-memory; `MCP_DEPLOYMENT_MODE=login_flow` assumes a single worker. Running with `uvicorn --workers N` will cause provisioning sessions to randomly fail. For higher concurrency, scale horizontally (multiple containers behind a sticky-session load balancer) rather than within a single process.
|
||||||
|
|
||||||
> **Sticky-session keying:** route on the **user identity** (e.g. the `sub` claim from the OAuth Bearer token) — **not** the raw token value, and **not** source IP. Bearer tokens rotate on refresh, which would silently break token-value affinity if a refresh lands between the request that initiates provisioning and the polling request that completes it. MCP clients may also not maintain stable IPs across those requests. A stable per-user identifier extracted from the `Authorization` header (e.g. `sub`) is the right key.
|
> **Sticky-session keying:** route on the **user identity** (e.g. the `sub` claim from the OAuth Bearer token) — **not** the raw token value, and **not** source IP. Bearer tokens rotate on refresh, which would silently break token-value affinity if a refresh lands between the request that initiates provisioning and the polling request that completes it. MCP clients may also not maintain stable IPs across those requests. A stable per-user identifier extracted from the `Authorization` header (e.g. `sub`) is the right key.
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ For multi-user deployment issues — provisioning loops, app-password storage, O
|
|||||||
```bash
|
```bash
|
||||||
# To Single-User BasicAuth: set NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD
|
# To Single-User BasicAuth: set NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD
|
||||||
# To Multi-User BasicAuth pass-through: ENABLE_MULTI_USER_BASIC_AUTH=true (no creds)
|
# To Multi-User BasicAuth pass-through: ENABLE_MULTI_USER_BASIC_AUTH=true (no creds)
|
||||||
# To Login Flow v2: ENABLE_LOGIN_FLOW=true (no creds)
|
# To Login Flow v2: MCP_DEPLOYMENT_MODE=login_flow (no creds; also the default fallback)
|
||||||
```
|
```
|
||||||
|
|
||||||
Restart the server after changing modes. The active mode is logged at startup; you can also set `MCP_DEPLOYMENT_MODE` explicitly to fail fast if the env vars don't match.
|
Restart the server after changing modes. The active mode is logged at startup; you can also set `MCP_DEPLOYMENT_MODE` explicitly to fail fast if the env vars don't match.
|
||||||
|
|||||||
+4
-2
@@ -7,8 +7,10 @@
|
|||||||
#
|
#
|
||||||
# Note: `login_flow` is the renamed successor of the former
|
# Note: `login_flow` is the renamed successor of the former
|
||||||
# `oauth_single_audience` mode (see ADR-022). The un-augmented OAuth path
|
# `oauth_single_audience` mode (see ADR-022). The un-augmented OAuth path
|
||||||
# required unmerged Nextcloud user_oidc patches and is no longer supported;
|
# required unmerged Nextcloud user_oidc patches and is no longer supported.
|
||||||
# `login_flow` mode now requires ENABLE_LOGIN_FLOW=true.
|
# Setting MCP_DEPLOYMENT_MODE=login_flow is sufficient — the Login Flow v2
|
||||||
|
# browser-based app-password layer is derived from the mode automatically
|
||||||
|
# (the previous ENABLE_LOGIN_FLOW=true env var has been removed).
|
||||||
#
|
#
|
||||||
# Recommendation: Set this for clarity and to catch configuration errors early
|
# Recommendation: Set this for clarity and to catch configuration errors early
|
||||||
#MCP_DEPLOYMENT_MODE=login_flow
|
#MCP_DEPLOYMENT_MODE=login_flow
|
||||||
|
|||||||
@@ -458,7 +458,10 @@ class Settings:
|
|||||||
# and passes them through to Nextcloud APIs (no storage, stateless)
|
# and passes them through to Nextcloud APIs (no storage, stateless)
|
||||||
enable_multi_user_basic_auth: bool = False
|
enable_multi_user_basic_auth: bool = False
|
||||||
|
|
||||||
# Login Flow v2 settings (ADR-022)
|
# Login Flow v2 derived flag (ADR-022). Internal — not user-settable.
|
||||||
|
# Auto-set by detect_auth_mode() when the resolved deployment mode is
|
||||||
|
# LOGIN_FLOW. Kept as a field for backward compat with the runtime call
|
||||||
|
# sites that read it (app.py, context.py, scope_authorization.py).
|
||||||
enable_login_flow: bool = False
|
enable_login_flow: bool = False
|
||||||
|
|
||||||
# Token and webhook storage settings
|
# Token and webhook storage settings
|
||||||
@@ -853,8 +856,10 @@ def get_settings() -> Settings:
|
|||||||
"userinfo_uri": "USERINFO_URI",
|
"userinfo_uri": "USERINFO_URI",
|
||||||
# Multi-user BasicAuth pass-through mode
|
# Multi-user BasicAuth pass-through mode
|
||||||
"enable_multi_user_basic_auth": "ENABLE_MULTI_USER_BASIC_AUTH",
|
"enable_multi_user_basic_auth": "ENABLE_MULTI_USER_BASIC_AUTH",
|
||||||
# Login Flow v2 settings (ADR-022)
|
# NOTE: `enable_login_flow` used to have an `ENABLE_LOGIN_FLOW` env-var
|
||||||
"enable_login_flow": "ENABLE_LOGIN_FLOW",
|
# alias here, but it was removed in the ADR-022 follow-up — the flag
|
||||||
|
# is now derived from MCP_DEPLOYMENT_MODE=login_flow and set by
|
||||||
|
# detect_auth_mode() so users only need to configure the mode.
|
||||||
# Token and webhook storage settings
|
# Token and webhook storage settings
|
||||||
"token_encryption_key": "TOKEN_ENCRYPTION_KEY",
|
"token_encryption_key": "TOKEN_ENCRYPTION_KEY",
|
||||||
"token_storage_db": "TOKEN_STORAGE_DB",
|
"token_storage_db": "TOKEN_STORAGE_DB",
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
|
|||||||
"Optional background sync using app passwords stored via Astrolabe.",
|
"Optional background sync using app passwords stored via Astrolabe.",
|
||||||
),
|
),
|
||||||
AuthMode.LOGIN_FLOW: ModeRequirements(
|
AuthMode.LOGIN_FLOW: ModeRequirements(
|
||||||
required=["nextcloud_host", "enable_login_flow"],
|
required=["nextcloud_host"],
|
||||||
optional=[
|
optional=[
|
||||||
# OAuth credentials (uses DCR if not provided)
|
# OAuth credentials (uses DCR if not provided)
|
||||||
"oidc_client_id",
|
"oidc_client_id",
|
||||||
@@ -201,24 +201,42 @@ def detect_auth_mode(settings: Settings) -> AuthMode:
|
|||||||
|
|
||||||
explicit_mode = mode_map[mode_str]
|
explicit_mode = mode_map[mode_str]
|
||||||
logger.info(f"Using explicit deployment mode: {explicit_mode.value}")
|
logger.info(f"Using explicit deployment mode: {explicit_mode.value}")
|
||||||
|
_sync_derived_flags(settings, explicit_mode)
|
||||||
return explicit_mode
|
return explicit_mode
|
||||||
|
|
||||||
# Auto-detection (existing behavior)
|
# Auto-detection (existing behavior)
|
||||||
# Check for multi-user BasicAuth
|
# Check for multi-user BasicAuth
|
||||||
if settings.enable_multi_user_basic_auth:
|
if settings.enable_multi_user_basic_auth:
|
||||||
|
_sync_derived_flags(settings, AuthMode.MULTI_USER_BASIC)
|
||||||
return AuthMode.MULTI_USER_BASIC
|
return AuthMode.MULTI_USER_BASIC
|
||||||
|
|
||||||
# Check for single-user BasicAuth (explicit credentials)
|
# Check for single-user BasicAuth (explicit credentials)
|
||||||
if settings.nextcloud_username and settings.nextcloud_password:
|
if settings.nextcloud_username and settings.nextcloud_password:
|
||||||
|
_sync_derived_flags(settings, AuthMode.SINGLE_USER_BASIC)
|
||||||
return AuthMode.SINGLE_USER_BASIC
|
return AuthMode.SINGLE_USER_BASIC
|
||||||
|
|
||||||
# Default: Login Flow v2 multi-user mode (browser-based app-password flow).
|
# Default: Login Flow v2 multi-user mode (browser-based app-password flow).
|
||||||
# The un-augmented OAuth bearer pass-through it replaced required unmerged
|
# The un-augmented OAuth bearer pass-through it replaced required unmerged
|
||||||
# Nextcloud user_oidc patches (see ADR-022); selecting LOGIN_FLOW without
|
# Nextcloud user_oidc patches (see ADR-022).
|
||||||
# ENABLE_LOGIN_FLOW=true fails validation below.
|
_sync_derived_flags(settings, AuthMode.LOGIN_FLOW)
|
||||||
return AuthMode.LOGIN_FLOW
|
return AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_derived_flags(settings: Settings, mode: AuthMode) -> None:
|
||||||
|
"""Derive internal feature flags from the resolved deployment mode.
|
||||||
|
|
||||||
|
Some runtime call sites (app.py, context.py, auth/scope_authorization.py)
|
||||||
|
still read individual boolean flags rather than passing the mode around.
|
||||||
|
Keep those flags in sync with the mode here so the mode is the single
|
||||||
|
source of truth and users don't have to set redundant env vars.
|
||||||
|
|
||||||
|
Specifically: `enable_login_flow` is now derived from
|
||||||
|
`mode == AuthMode.LOGIN_FLOW`. The ENABLE_LOGIN_FLOW env-var alias was
|
||||||
|
removed in the ADR-022 follow-up (PR #787).
|
||||||
|
"""
|
||||||
|
settings.enable_login_flow = mode == AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
|
|
||||||
def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
||||||
"""Validate configuration for detected mode.
|
"""Validate configuration for detected mode.
|
||||||
|
|
||||||
@@ -308,16 +326,12 @@ def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if mode == AuthMode.LOGIN_FLOW:
|
if mode == AuthMode.LOGIN_FLOW:
|
||||||
# ADR-022: LOGIN_FLOW requires the Login Flow v2 layer. The un-augmented
|
# ADR-022 follow-up: the un-augmented OAuth bearer pass-through (the
|
||||||
# OAuth bearer pass-through (formerly OAUTH_SINGLE_AUDIENCE without
|
# old OAUTH_SINGLE_AUDIENCE without ENABLE_LOGIN_FLOW) needed unmerged
|
||||||
# ENABLE_LOGIN_FLOW) needed unmerged Nextcloud user_oidc patches and
|
# Nextcloud user_oidc patches and is no longer supported. The
|
||||||
# is no longer supported.
|
# `enable_login_flow` flag is now derived from the resolved mode by
|
||||||
if not settings.enable_login_flow:
|
# `_sync_derived_flags`, so users only configure the mode — no
|
||||||
errors.append(
|
# separate ENABLE_LOGIN_FLOW env var is needed.
|
||||||
f"[{mode.value}] ENABLE_LOGIN_FLOW=true is required for "
|
|
||||||
"login_flow mode. The un-augmented OAuth path is no longer "
|
|
||||||
"supported — see ADR-022."
|
|
||||||
)
|
|
||||||
|
|
||||||
# If OAuth credentials not provided, DCR must be available
|
# If OAuth credentials not provided, DCR must be available
|
||||||
# (This is a runtime check, not a config check, so we just warn)
|
# (This is a runtime check, not a config check, so we just warn)
|
||||||
|
|||||||
@@ -289,22 +289,22 @@ class TestLoginFlowValidation:
|
|||||||
"""Test validation for Login Flow v2 mode (formerly OAUTH_SINGLE_AUDIENCE)."""
|
"""Test validation for Login Flow v2 mode (formerly OAUTH_SINGLE_AUDIENCE)."""
|
||||||
|
|
||||||
def test_valid_minimal_config(self):
|
def test_valid_minimal_config(self):
|
||||||
"""Test valid minimal Login Flow v2 config."""
|
"""Test valid minimal Login Flow v2 config — enable_login_flow is now derived."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_login_flow=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.LOGIN_FLOW
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
assert len(errors) == 0
|
assert len(errors) == 0
|
||||||
|
# ADR-022 follow-up: enable_login_flow is derived from the resolved mode.
|
||||||
|
assert settings.enable_login_flow is True
|
||||||
|
|
||||||
def test_valid_with_static_credentials(self):
|
def test_valid_with_static_credentials(self):
|
||||||
"""Test valid config with static OAuth credentials."""
|
"""Test valid config with static OAuth credentials."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_login_flow=True,
|
|
||||||
oidc_client_id="test-client",
|
oidc_client_id="test-client",
|
||||||
oidc_client_secret="test-secret",
|
oidc_client_secret="test-secret",
|
||||||
)
|
)
|
||||||
@@ -318,7 +318,6 @@ class TestLoginFlowValidation:
|
|||||||
"""Test valid config with offline access."""
|
"""Test valid config with offline access."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_login_flow=True,
|
|
||||||
oidc_client_id="test-client",
|
oidc_client_id="test-client",
|
||||||
oidc_client_secret="test-secret",
|
oidc_client_secret="test-secret",
|
||||||
enable_offline_access=True,
|
enable_offline_access=True,
|
||||||
@@ -348,7 +347,6 @@ class TestLoginFlowValidation:
|
|||||||
"""Test error when offline access enabled but encryption key missing."""
|
"""Test error when offline access enabled but encryption key missing."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_login_flow=True,
|
|
||||||
enable_offline_access=True,
|
enable_offline_access=True,
|
||||||
token_storage_db="/tmp/tokens.db",
|
token_storage_db="/tmp/tokens.db",
|
||||||
)
|
)
|
||||||
@@ -358,19 +356,28 @@ class TestLoginFlowValidation:
|
|||||||
assert mode == AuthMode.LOGIN_FLOW
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
assert any("token_encryption_key" in err.lower() for err in errors)
|
assert any("token_encryption_key" in err.lower() for err in errors)
|
||||||
|
|
||||||
def test_login_flow_requires_enable_login_flow_flag(self):
|
def test_login_flow_mode_auto_derives_enable_login_flow_flag(self):
|
||||||
"""ADR-022: LOGIN_FLOW mode must error when ENABLE_LOGIN_FLOW is not true."""
|
"""ADR-022 follow-up: setting MCP_DEPLOYMENT_MODE=login_flow auto-derives the flag.
|
||||||
settings = Settings(
|
|
||||||
nextcloud_host="http://localhost",
|
|
||||||
# enable_login_flow deliberately omitted (defaults to False)
|
|
||||||
)
|
|
||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
|
||||||
|
|
||||||
|
Users no longer need to set ENABLE_LOGIN_FLOW=true (env var was removed);
|
||||||
|
detect_auth_mode populates settings.enable_login_flow from the resolved mode.
|
||||||
|
"""
|
||||||
|
# Default-fallback case: no auth env vars → LOGIN_FLOW.
|
||||||
|
settings = Settings(nextcloud_host="http://localhost")
|
||||||
|
assert settings.enable_login_flow is False # default before detection
|
||||||
|
mode = detect_auth_mode(settings)
|
||||||
assert mode == AuthMode.LOGIN_FLOW
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
assert any("ENABLE_LOGIN_FLOW" in err for err in errors), (
|
assert settings.enable_login_flow is True
|
||||||
f"Expected ENABLE_LOGIN_FLOW gate error, got: {errors}"
|
|
||||||
|
# Non-LOGIN_FLOW mode should leave the flag False.
|
||||||
|
basic_settings = Settings(
|
||||||
|
nextcloud_host="http://localhost",
|
||||||
|
nextcloud_username="alice",
|
||||||
|
nextcloud_password="hunter2",
|
||||||
)
|
)
|
||||||
|
basic_mode = detect_auth_mode(basic_settings)
|
||||||
|
assert basic_mode == AuthMode.SINGLE_USER_BASIC
|
||||||
|
assert basic_settings.enable_login_flow is False
|
||||||
|
|
||||||
def test_vector_sync_auto_enables_background_ops_in_login_flow_mode(self):
|
def test_vector_sync_auto_enables_background_ops_in_login_flow_mode(self):
|
||||||
"""Test vector sync automatically enables background operations in Login Flow v2 mode (ADR-021)."""
|
"""Test vector sync automatically enables background operations in Login Flow v2 mode (ADR-021)."""
|
||||||
@@ -380,7 +387,6 @@ class TestLoginFlowValidation:
|
|||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NEXTCLOUD_HOST": "http://localhost:8080",
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
"ENABLE_LOGIN_FLOW": "true",
|
|
||||||
"VECTOR_SYNC_ENABLED": "true",
|
"VECTOR_SYNC_ENABLED": "true",
|
||||||
"QDRANT_LOCATION": ":memory:",
|
"QDRANT_LOCATION": ":memory:",
|
||||||
"OLLAMA_BASE_URL": "http://ollama:11434",
|
"OLLAMA_BASE_URL": "http://ollama:11434",
|
||||||
|
|||||||
Reference in New Issue
Block a user