refactor(config)!: drop ENABLE_MULTI_USER_BASIC_AUTH env var, fail loud on legacy aliases
Same pattern as the ENABLE_LOGIN_FLOW removal in the previous commit:
the deployment mode (MCP_DEPLOYMENT_MODE) is the single source of truth
for selecting an auth flow. The ENABLE_MULTI_USER_BASIC_AUTH env-var
alias is redundant with `MCP_DEPLOYMENT_MODE=multi_user_basic`.
Unlike the ENABLE_LOGIN_FLOW removal — where silent removal was safe
because Login Flow v2 is the auto-detection default — silent removal
here would be a surprise: a user with only ENABLE_MULTI_USER_BASIC_AUTH=true
in their .env would auto-detect into LOGIN_FLOW after upgrade (wrong
runtime mode). Mitigation: detect_auth_mode now reads os.environ
directly for both legacy aliases and raises ValueError with a one-line
migration message if either is set. Applied retroactively to
ENABLE_LOGIN_FLOW as well — loud is better than silent.
- nextcloud_mcp_server/config.py:
- Drop the dynaconf env-var alias entry for ENABLE_MULTI_USER_BASIC_AUTH.
- Update the `enable_multi_user_basic_auth` field docstring to mark it
as derived / not user-settable.
- `_is_multi_user_mode()` (early-config helper, runs before Settings
is built) switched to checking MCP_DEPLOYMENT_MODE directly. Now
consistent with the canonical detection in detect_auth_mode.
- nextcloud_mcp_server/config_validators.py:
- Drop the auto-detection branch (`if settings.enable_multi_user_basic_auth`).
Selection of MULTI_USER_BASIC is now exclusively via the explicit
MCP_DEPLOYMENT_MODE branch.
- Add `enable_multi_user_basic_auth` to `_sync_derived_flags` alongside
`enable_login_flow` — both flags are now derived from the resolved mode.
- Drop `enable_multi_user_basic_auth` from
`MODE_REQUIREMENTS[MULTI_USER_BASIC].required` and from the
`forbidden` lists of SINGLE_USER_BASIC and LOGIN_FLOW (no longer
user input → no meaningful forbidden check).
- Add loud-deprecation `ValueError` block at the top of detect_auth_mode
that errors with a clear migration message when ENABLE_MULTI_USER_BASIC_AUTH
or ENABLE_LOGIN_FLOW is found in os.environ.
- tests/unit/test_config_validators.py:
- Switch ~10 fixtures from `enable_multi_user_basic_auth=True` to
`deployment_mode="multi_user_basic"` (mirrors `enable_login_flow`
treatment from the previous commit).
- Switch two `patch.dict(os.environ, {"ENABLE_MULTI_USER_BASIC_AUTH": "true"})`
blocks to use MCP_DEPLOYMENT_MODE.
- Rename `test_forbidden_multi_user_basic_auth` to
`test_forbidden_multi_user_basic_when_credentials_present` — the
scenario is now an explicit-mode + credentials conflict, not an
env-var-flag conflict.
- Add `test_legacy_enable_multi_user_basic_auth_env_var_errors` and
`test_legacy_enable_login_flow_env_var_errors` to exercise the new
loud-deprecation ValueError path.
- docker-compose.yml: mcp-multi-user-basic profile switched to
`MCP_DEPLOYMENT_MODE=multi_user_basic`.
- env.sample: replaced `#ENABLE_MULTI_USER_BASIC_AUTH=true` example with
`#MCP_DEPLOYMENT_MODE=multi_user_basic`.
- docs/authentication.md, configuration.md, troubleshooting.md,
auth-flows.md, webhook-management-guide.md,
configuration-migration-v2.md, ADR-025: replaced env-var examples
with the canonical MCP_DEPLOYMENT_MODE form.
- docs/ADR-020: marked partly superseded by ADR-022.
- CLAUDE.md: Multi-User BasicAuth section updated to set
MCP_DEPLOYMENT_MODE.
- nextcloud_mcp_server/vector/oauth_sync.py: module docstring updated.
BREAKING CHANGE: ENABLE_MULTI_USER_BASIC_AUTH is no longer read from
the environment, and setting it now raises a startup ValueError with
a migration message. Replace `ENABLE_MULTI_USER_BASIC_AUTH=true` with
`MCP_DEPLOYMENT_MODE=multi_user_basic`. The same loud-deprecation
check is also applied to the recently-removed ENABLE_LOGIN_FLOW —
replace with `MCP_DEPLOYMENT_MODE=login_flow` (or drop both;
`login_flow` is the auto-detect 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
df4994e860
commit
282c245da1
@@ -338,7 +338,7 @@ The server supports three deployment modes, controlled by environment variables
|
|||||||
- Best for: personal instances, local development
|
- Best for: personal instances, local development
|
||||||
|
|
||||||
**2. Multi-User BasicAuth** (profile: `multi-user-basic`)
|
**2. Multi-User BasicAuth** (profile: `multi-user-basic`)
|
||||||
- Set `ENABLE_MULTI_USER_BASIC_AUTH=true`
|
- Set `MCP_DEPLOYMENT_MODE=multi_user_basic`
|
||||||
- Each MCP client provides credentials via HTTP Authorization header
|
- Each MCP client provides credentials via HTTP Authorization header
|
||||||
- Per-request client creation from extracted credentials
|
- Per-request client creation from extracted credentials
|
||||||
- Best for: internal deployments where users manage their own Nextcloud credentials
|
- Best for: internal deployments where users manage their own Nextcloud credentials
|
||||||
|
|||||||
+2
-2
@@ -146,11 +146,11 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8003:8000
|
- 127.0.0.1:8003:8000
|
||||||
environment:
|
environment:
|
||||||
# Multi-user BasicAuth pass-through mode (ADR-020)
|
# Multi-user BasicAuth pass-through mode (ADR-020, ADR-022)
|
||||||
- NEXTCLOUD_HOST=http://app:80
|
- NEXTCLOUD_HOST=http://app:80
|
||||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8003
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8003
|
||||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||||
- ENABLE_MULTI_USER_BASIC_AUTH=true
|
- MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
- ENABLE_BACKGROUND_OPERATIONS=true
|
- ENABLE_BACKGROUND_OPERATIONS=true
|
||||||
|
|
||||||
# Token storage (required for middleware initialization).
|
# Token storage (required for middleware initialization).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# ADR-020: Deployment Modes and Configuration Validation
|
# ADR-020: Deployment Modes and Configuration Validation
|
||||||
|
|
||||||
**Status:** Accepted
|
**Status:** Accepted — partly superseded by ADR-022 (`oauth_single_audience` renamed to `login_flow`; the `ENABLE_MULTI_USER_BASIC_AUTH` and `ENABLE_LOGIN_FLOW` env-var aliases were removed in favour of `MCP_DEPLOYMENT_MODE` as the single source of truth)
|
||||||
**Date:** 2025-12-20
|
**Date:** 2025-12-20
|
||||||
**Deciders:** Development Team
|
**Deciders:** Development Team
|
||||||
**Related:** ADR-002 (Vector Sync), ADR-004 (Progressive Consent), ADR-019 (Multi-user BasicAuth)
|
**Related:** ADR-002 (Vector Sync), ADR-004 (Progressive Consent), ADR-019 (Multi-user BasicAuth), ADR-022 (Deployment Mode Consolidation)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -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 | 2 | `MCP_DEPLOYMENT_MODE`, `ENABLE_MULTI_USER_BASIC_AUTH` |
|
| Mode Selection | 1 | `MCP_DEPLOYMENT_MODE` |
|
||||||
| 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` |
|
||||||
@@ -114,9 +114,10 @@ nextcloud_ca_bundle = "@none"
|
|||||||
# mcp_deployment_mode = ""
|
# mcp_deployment_mode = ""
|
||||||
|
|
||||||
# === Authentication Toggles ===
|
# === Authentication Toggles ===
|
||||||
enable_multi_user_basic_auth = false
|
# Both `enable_multi_user_basic_auth` and `enable_login_flow` are derived
|
||||||
# `enable_login_flow` is derived from MCP_DEPLOYMENT_MODE=login_flow in
|
# from MCP_DEPLOYMENT_MODE in detect_auth_mode (ADR-022 follow-up) — no
|
||||||
# detect_auth_mode (ADR-022 follow-up) — no separate toggle.
|
# separate toggles. Only ENABLE_TOKEN_EXCHANGE remains as an independent
|
||||||
|
# flag (separate cleanup).
|
||||||
enable_token_exchange = false
|
enable_token_exchange = false
|
||||||
|
|
||||||
# === Token Storage ===
|
# === Token Storage ===
|
||||||
@@ -199,7 +200,7 @@ nextcloud_mcp_port = 8000
|
|||||||
# nextcloud_password = "" (in .secrets.toml)
|
# nextcloud_password = "" (in .secrets.toml)
|
||||||
|
|
||||||
[multi_user_basic]
|
[multi_user_basic]
|
||||||
enable_multi_user_basic_auth = true
|
# enable_multi_user_basic_auth is now derived from the mode (ADR-022 follow-up).
|
||||||
token_storage_db = "/app/data/tokens.db"
|
token_storage_db = "/app/data/tokens.db"
|
||||||
|
|
||||||
[login_flow]
|
[login_flow]
|
||||||
@@ -344,10 +345,16 @@ In **Phase 4**, this could migrate to a post-hook:
|
|||||||
# Phase 4 target (not implemented in Phases 1-3)
|
# Phase 4 target (not implemented in Phases 1-3)
|
||||||
def resolve_dependencies(settings):
|
def resolve_dependencies(settings):
|
||||||
"""Auto-enable background operations for semantic search in multi-user modes."""
|
"""Auto-enable background operations for semantic search in multi-user modes."""
|
||||||
|
mode = (settings.get("MCP_DEPLOYMENT_MODE", "") or "").lower().strip()
|
||||||
is_multi_user = (
|
is_multi_user = (
|
||||||
settings.get("ENABLE_MULTI_USER_BASIC_AUTH", False)
|
mode in {"multi_user_basic", "login_flow"}
|
||||||
or settings.get("ENABLE_TOKEN_EXCHANGE", False)
|
or settings.get("ENABLE_TOKEN_EXCHANGE", False)
|
||||||
or (not settings.get("NEXTCLOUD_USERNAME") and not settings.get("NEXTCLOUD_PASSWORD"))
|
or (
|
||||||
|
mode != "single_user_basic"
|
||||||
|
and not (
|
||||||
|
settings.get("NEXTCLOUD_USERNAME") and settings.get("NEXTCLOUD_PASSWORD")
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if settings.get("ENABLE_SEMANTIC_SEARCH", False) and is_multi_user:
|
if settings.get("ENABLE_SEMANTIC_SEARCH", False) and is_multi_user:
|
||||||
if not settings.get("ENABLE_BACKGROUND_OPERATIONS", False):
|
if not settings.get("ENABLE_BACKGROUND_OPERATIONS", False):
|
||||||
|
|||||||
+1
-1
@@ -221,7 +221,7 @@ NEXTCLOUD_PASSWORD=<app-password>
|
|||||||
### Multi-User BasicAuth
|
### Multi-User BasicAuth
|
||||||
```bash
|
```bash
|
||||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
|
|
||||||
# Optional: app-password storage for background sync
|
# Optional: app-password storage for background sync
|
||||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Each MCP client sends its own credentials in an HTTP `Authorization: Basic` head
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
NEXTCLOUD_HOST=https://your.nextcloud.example.com
|
NEXTCLOUD_HOST=https://your.nextcloud.example.com
|
||||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
```
|
```
|
||||||
|
|
||||||
`NEXTCLOUD_USERNAME` and `NEXTCLOUD_PASSWORD` must NOT be set in this mode.
|
`NEXTCLOUD_USERNAME` and `NEXTCLOUD_PASSWORD` must NOT be set in this mode.
|
||||||
@@ -74,7 +74,7 @@ The server detects the active mode from environment variables at startup:
|
|||||||
| Env vars present | Detected mode |
|
| Env vars present | Detected mode |
|
||||||
|------------------|---------------|
|
|------------------|---------------|
|
||||||
| `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) |
|
| `MCP_DEPLOYMENT_MODE=multi_user_basic` | Multi-User (BasicAuth pass-through) |
|
||||||
| `MCP_DEPLOYMENT_MODE=login_flow` or no auth env vars set | 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:
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ NEXTCLOUD_OIDC_CLIENT_SECRET=secret
|
|||||||
**Before (v0.57.x):**
|
**Before (v0.57.x):**
|
||||||
```bash
|
```bash
|
||||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
|
|
||||||
# Both required - redundant
|
# Both required - redundant
|
||||||
ENABLE_OFFLINE_ACCESS=true
|
ENABLE_OFFLINE_ACCESS=true
|
||||||
@@ -205,7 +205,7 @@ NEXTCLOUD_OIDC_CLIENT_SECRET=secret
|
|||||||
**After (v0.58.0+ - Simplified):**
|
**After (v0.58.0+ - Simplified):**
|
||||||
```bash
|
```bash
|
||||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
|
|
||||||
# Optional: Explicit mode declaration
|
# Optional: Explicit mode declaration
|
||||||
MCP_DEPLOYMENT_MODE=multi_user_basic
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
@@ -448,7 +448,7 @@ Server activates `login_flow` mode when you expected `multi_user_basic`
|
|||||||
Add explicit mode declaration:
|
Add explicit mode declaration:
|
||||||
```bash
|
```bash
|
||||||
MCP_DEPLOYMENT_MODE=multi_user_basic
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ Each MCP client sends its own Nextcloud credentials in an `Authorization: Basic`
|
|||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
|
|
||||||
# Optional: enable per-user app-password storage for background sync
|
# Optional: enable per-user app-password storage for background sync
|
||||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||||
|
|||||||
@@ -147,7 +147,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: MCP_DEPLOYMENT_MODE=multi_user_basic (no creds)
|
||||||
# To Login Flow v2: MCP_DEPLOYMENT_MODE=login_flow (no creds; also the default fallback)
|
# To Login Flow v2: MCP_DEPLOYMENT_MODE=login_flow (no creds; also the default fallback)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ php occ webhook_listeners:remove <webhook-id>
|
|||||||
**Configuration:**
|
**Configuration:**
|
||||||
```bash
|
```bash
|
||||||
NEXTCLOUD_HOST=http://nextcloud.example.com
|
NEXTCLOUD_HOST=http://nextcloud.example.com
|
||||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
ENABLE_BACKGROUND_OPERATIONS=true
|
ENABLE_BACKGROUND_OPERATIONS=true
|
||||||
TOKEN_ENCRYPTION_KEY=<key>
|
TOKEN_ENCRYPTION_KEY=<key>
|
||||||
TOKEN_STORAGE_DB=/app/data/tokens.db
|
TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||||
|
|||||||
+3
-2
@@ -49,8 +49,9 @@ NEXTCLOUD_PASSWORD=
|
|||||||
# Users provide credentials in request headers (pass-through)
|
# Users provide credentials in request headers (pass-through)
|
||||||
# Use for: Multi-user without OAuth, simple shared deployments
|
# Use for: Multi-user without OAuth, simple shared deployments
|
||||||
#
|
#
|
||||||
# Required:
|
# Required (sets the deployment mode; the legacy ENABLE_MULTI_USER_BASIC_AUTH
|
||||||
#ENABLE_MULTI_USER_BASIC_AUTH=true
|
# env var was removed in the ADR-022 follow-up):
|
||||||
|
#MCP_DEPLOYMENT_MODE=multi_user_basic
|
||||||
#
|
#
|
||||||
# Optional - Background Operations (for semantic search, future features):
|
# Optional - Background Operations (for semantic search, future features):
|
||||||
# Enable background token storage using app passwords (via Astrolabe)
|
# Enable background token storage using app passwords (via Astrolabe)
|
||||||
|
|||||||
@@ -453,9 +453,13 @@ class Settings:
|
|||||||
# Progressive Consent settings (always enabled - no flag needed)
|
# Progressive Consent settings (always enabled - no flag needed)
|
||||||
enable_offline_access: bool = False
|
enable_offline_access: bool = False
|
||||||
|
|
||||||
# Multi-user BasicAuth pass-through mode (ADR-019 interim solution)
|
# Multi-user BasicAuth pass-through mode (ADR-019 interim solution).
|
||||||
# When enabled, MCP server extracts BasicAuth credentials from request headers
|
# Internal — not user-settable; the ENABLE_MULTI_USER_BASIC_AUTH env-var
|
||||||
# and passes them through to Nextcloud APIs (no storage, stateless)
|
# alias was removed in the ADR-022 follow-up. Auto-set by
|
||||||
|
# detect_auth_mode() when MCP_DEPLOYMENT_MODE=multi_user_basic. When True,
|
||||||
|
# the MCP server extracts BasicAuth credentials from request headers and
|
||||||
|
# passes them through to Nextcloud APIs (no storage, stateless). Kept
|
||||||
|
# as a field for backward compat with the runtime call sites that read it.
|
||||||
enable_multi_user_basic_auth: bool = False
|
enable_multi_user_basic_auth: bool = False
|
||||||
|
|
||||||
# Login Flow v2 derived flag (ADR-022). Internal — not user-settable.
|
# Login Flow v2 derived flag (ADR-022). Internal — not user-settable.
|
||||||
@@ -723,20 +727,31 @@ def _get_semantic_search_enabled() -> bool:
|
|||||||
def _is_multi_user_mode() -> bool:
|
def _is_multi_user_mode() -> bool:
|
||||||
"""Detect if this is a multi-user deployment mode.
|
"""Detect if this is a multi-user deployment mode.
|
||||||
|
|
||||||
|
Runs early in config setup (before Settings is fully built) for
|
||||||
|
mode-conditional defaults. Must match the canonical detection in
|
||||||
|
`config_validators.detect_auth_mode`, but works directly against the
|
||||||
|
raw dynaconf store since Settings doesn't exist yet.
|
||||||
|
|
||||||
Multi-user modes are:
|
Multi-user modes are:
|
||||||
- Multi-user BasicAuth (ENABLE_MULTI_USER_BASIC_AUTH=true)
|
- Multi-user BasicAuth (MCP_DEPLOYMENT_MODE=multi_user_basic)
|
||||||
- OAuth Single-Audience (no username/password set)
|
- Login Flow v2 / default OAuth (MCP_DEPLOYMENT_MODE=login_flow, or no
|
||||||
|
username/password and no explicit mode)
|
||||||
- OAuth Token Exchange (ENABLE_TOKEN_EXCHANGE=true)
|
- OAuth Token Exchange (ENABLE_TOKEN_EXCHANGE=true)
|
||||||
|
|
||||||
Single-user modes are:
|
Single-user mode is:
|
||||||
- Single-user BasicAuth (username and password both set)
|
- Single-user BasicAuth (username and password both set)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if multi-user mode detected
|
True if multi-user mode detected
|
||||||
"""
|
"""
|
||||||
# Multi-user BasicAuth explicitly enabled
|
# Explicit deployment mode wins. The ENABLE_MULTI_USER_BASIC_AUTH env-var
|
||||||
if _dynaconf.get("ENABLE_MULTI_USER_BASIC_AUTH", False):
|
# alias was removed in the ADR-022 follow-up; selection is now via
|
||||||
|
# MCP_DEPLOYMENT_MODE.
|
||||||
|
explicit_mode = str(_dynaconf.get("MCP_DEPLOYMENT_MODE", "") or "").lower().strip()
|
||||||
|
if explicit_mode in {"multi_user_basic", "login_flow"}:
|
||||||
return True
|
return True
|
||||||
|
if explicit_mode == "single_user_basic":
|
||||||
|
return False
|
||||||
|
|
||||||
# Token exchange implies OAuth multi-user
|
# Token exchange implies OAuth multi-user
|
||||||
if _dynaconf.get("ENABLE_TOKEN_EXCHANGE", False):
|
if _dynaconf.get("ENABLE_TOKEN_EXCHANGE", False):
|
||||||
@@ -748,7 +763,7 @@ def _is_multi_user_mode() -> bool:
|
|||||||
if has_username and has_password:
|
if has_username and has_password:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Otherwise, assume OAuth multi-user (default when no credentials provided)
|
# Otherwise, assume multi-user (default when no credentials provided)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -854,12 +869,10 @@ def get_settings() -> Settings:
|
|||||||
"jwks_uri": "JWKS_URI",
|
"jwks_uri": "JWKS_URI",
|
||||||
"introspection_uri": "INTROSPECTION_URI",
|
"introspection_uri": "INTROSPECTION_URI",
|
||||||
"userinfo_uri": "USERINFO_URI",
|
"userinfo_uri": "USERINFO_URI",
|
||||||
# Multi-user BasicAuth pass-through mode
|
# NOTE: `enable_multi_user_basic_auth` and `enable_login_flow` no
|
||||||
"enable_multi_user_basic_auth": "ENABLE_MULTI_USER_BASIC_AUTH",
|
# longer have env-var aliases — both are derived from the resolved
|
||||||
# NOTE: `enable_login_flow` used to have an `ENABLE_LOGIN_FLOW` env-var
|
# MCP_DEPLOYMENT_MODE in detect_auth_mode() so users only configure
|
||||||
# alias here, but it was removed in the ADR-022 follow-up — the flag
|
# the mode (ADR-022 follow-up).
|
||||||
# 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",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ See ADR-020 for detailed architecture and deployment mode documentation.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
@@ -64,7 +65,6 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
|
|||||||
"document_chunk_overlap",
|
"document_chunk_overlap",
|
||||||
],
|
],
|
||||||
forbidden=[
|
forbidden=[
|
||||||
"enable_multi_user_basic_auth",
|
|
||||||
"oidc_client_id",
|
"oidc_client_id",
|
||||||
"oidc_client_secret",
|
"oidc_client_secret",
|
||||||
],
|
],
|
||||||
@@ -78,7 +78,7 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
|
|||||||
"Suitable for personal Nextcloud instances and local development.",
|
"Suitable for personal Nextcloud instances and local development.",
|
||||||
),
|
),
|
||||||
AuthMode.MULTI_USER_BASIC: ModeRequirements(
|
AuthMode.MULTI_USER_BASIC: ModeRequirements(
|
||||||
required=["nextcloud_host", "enable_multi_user_basic_auth"],
|
required=["nextcloud_host"],
|
||||||
optional=[
|
optional=[
|
||||||
# Background sync with app passwords (via Astrolabe)
|
# Background sync with app passwords (via Astrolabe)
|
||||||
"enable_offline_access",
|
"enable_offline_access",
|
||||||
@@ -138,7 +138,6 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
|
|||||||
forbidden=[
|
forbidden=[
|
||||||
"nextcloud_username",
|
"nextcloud_username",
|
||||||
"nextcloud_password",
|
"nextcloud_password",
|
||||||
"enable_multi_user_basic_auth",
|
|
||||||
],
|
],
|
||||||
conditional={
|
conditional={
|
||||||
"enable_offline_access": [
|
"enable_offline_access": [
|
||||||
@@ -181,6 +180,21 @@ def detect_auth_mode(settings: Settings) -> AuthMode:
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ADR-022 follow-up: fail loudly if a caller is still relying on the
|
||||||
|
# removed env-var aliases. Bypass dynaconf and read os.environ directly
|
||||||
|
# so the check survives even though the aliases are gone.
|
||||||
|
for legacy, replacement in (
|
||||||
|
("ENABLE_MULTI_USER_BASIC_AUTH", "multi_user_basic"),
|
||||||
|
("ENABLE_LOGIN_FLOW", "login_flow"),
|
||||||
|
):
|
||||||
|
if os.getenv(legacy):
|
||||||
|
raise ValueError(
|
||||||
|
f"{legacy} is no longer read from the environment. "
|
||||||
|
f"Set MCP_DEPLOYMENT_MODE={replacement} instead "
|
||||||
|
"(ADR-022). The deployment mode is the single source of "
|
||||||
|
"truth for selecting an auth flow."
|
||||||
|
)
|
||||||
|
|
||||||
# ADR-021: Check for explicit deployment mode first
|
# ADR-021: Check for explicit deployment mode first
|
||||||
if settings.deployment_mode:
|
if settings.deployment_mode:
|
||||||
mode_str = settings.deployment_mode.lower().strip()
|
mode_str = settings.deployment_mode.lower().strip()
|
||||||
@@ -204,11 +218,11 @@ def detect_auth_mode(settings: Settings) -> AuthMode:
|
|||||||
_sync_derived_flags(settings, explicit_mode)
|
_sync_derived_flags(settings, explicit_mode)
|
||||||
return explicit_mode
|
return explicit_mode
|
||||||
|
|
||||||
# Auto-detection (existing behavior)
|
# Auto-detection (no explicit deployment_mode).
|
||||||
# Check for multi-user BasicAuth
|
# MULTI_USER_BASIC is no longer auto-detectable — the ENABLE_MULTI_USER_BASIC_AUTH
|
||||||
if settings.enable_multi_user_basic_auth:
|
# env-var alias was dropped in the ADR-022 follow-up, so the only way to
|
||||||
_sync_derived_flags(settings, AuthMode.MULTI_USER_BASIC)
|
# opt into that mode is `MCP_DEPLOYMENT_MODE=multi_user_basic` (handled
|
||||||
return AuthMode.MULTI_USER_BASIC
|
# above). The legacy env var fails loudly at the top of this function.
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -228,13 +242,12 @@ def _sync_derived_flags(settings: Settings, mode: AuthMode) -> None:
|
|||||||
Some runtime call sites (app.py, context.py, auth/scope_authorization.py)
|
Some runtime call sites (app.py, context.py, auth/scope_authorization.py)
|
||||||
still read individual boolean flags rather than passing the mode around.
|
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
|
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.
|
source of truth and users don't have to set redundant env vars. The
|
||||||
|
ENABLE_LOGIN_FLOW and ENABLE_MULTI_USER_BASIC_AUTH env-var aliases were
|
||||||
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).
|
removed in the ADR-022 follow-up (PR #787).
|
||||||
"""
|
"""
|
||||||
settings.enable_login_flow = mode == AuthMode.LOGIN_FLOW
|
settings.enable_login_flow = mode == AuthMode.LOGIN_FLOW
|
||||||
|
settings.enable_multi_user_basic_auth = mode == AuthMode.MULTI_USER_BASIC
|
||||||
|
|
||||||
|
|
||||||
def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Manages background vector sync for multi-user deployments:
|
|||||||
|
|
||||||
Authentication strategies are mutually exclusive by deployment mode:
|
Authentication strategies are mutually exclusive by deployment mode:
|
||||||
|
|
||||||
Multi-user BasicAuth mode (ENABLE_MULTI_USER_BASIC_AUTH=true):
|
Multi-user BasicAuth mode (MCP_DEPLOYMENT_MODE=multi_user_basic):
|
||||||
- Uses app passwords stored locally in MCP server's database
|
- Uses app passwords stored locally in MCP server's database
|
||||||
- Users provision via Astrolabe personal settings, which sends to MCP API
|
- Users provision via Astrolabe personal settings, which sends to MCP API
|
||||||
- OAuth is NOT used
|
- OAuth is NOT used
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Tests cover:
|
|||||||
import os
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from nextcloud_mcp_server.config import Settings, _reload_config
|
from nextcloud_mcp_server.config import Settings, _reload_config
|
||||||
from nextcloud_mcp_server.config_validators import (
|
from nextcloud_mcp_server.config_validators import (
|
||||||
AuthMode,
|
AuthMode,
|
||||||
@@ -23,14 +25,21 @@ class TestModeDetection:
|
|||||||
"""Test auth mode detection from configuration."""
|
"""Test auth mode detection from configuration."""
|
||||||
|
|
||||||
def test_multi_user_basic_mode_detection(self):
|
def test_multi_user_basic_mode_detection(self):
|
||||||
"""Test multi-user BasicAuth mode is detected."""
|
"""Test multi-user BasicAuth mode is selected via explicit deployment_mode.
|
||||||
|
|
||||||
|
ADR-022 follow-up: the ENABLE_MULTI_USER_BASIC_AUTH auto-detection branch
|
||||||
|
was removed; the only way to opt in is `MCP_DEPLOYMENT_MODE=multi_user_basic`.
|
||||||
|
Coverage for the explicit-mode path also lives in
|
||||||
|
TestExplicitModeSelection::test_explicit_multi_user_basic_mode.
|
||||||
|
"""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
)
|
)
|
||||||
|
|
||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
assert mode == AuthMode.MULTI_USER_BASIC
|
assert mode == AuthMode.MULTI_USER_BASIC
|
||||||
|
assert settings.enable_multi_user_basic_auth is True
|
||||||
|
|
||||||
def test_single_user_basic_mode_detection(self):
|
def test_single_user_basic_mode_detection(self):
|
||||||
"""Test single-user BasicAuth mode is detected."""
|
"""Test single-user BasicAuth mode is detected."""
|
||||||
@@ -127,20 +136,25 @@ class TestSingleUserBasicValidation:
|
|||||||
# In OAuth mode, having a username set is forbidden
|
# In OAuth mode, having a username set is forbidden
|
||||||
assert any("nextcloud_username" in err.lower() for err in errors)
|
assert any("nextcloud_username" in err.lower() for err in errors)
|
||||||
|
|
||||||
def test_forbidden_multi_user_basic_auth(self):
|
def test_forbidden_multi_user_basic_when_credentials_present(self):
|
||||||
"""Test error when ENABLE_MULTI_USER_BASIC_AUTH is set."""
|
"""Test multi-user mode rejects single-user credentials.
|
||||||
|
|
||||||
|
When MCP_DEPLOYMENT_MODE=multi_user_basic is set explicitly but
|
||||||
|
NEXTCLOUD_USERNAME/PASSWORD are also set (a misconfiguration),
|
||||||
|
the explicit mode wins and validation reports the credentials as
|
||||||
|
forbidden.
|
||||||
|
"""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
nextcloud_username="admin",
|
nextcloud_username="admin",
|
||||||
nextcloud_password="password",
|
nextcloud_password="password",
|
||||||
enable_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Note: This will detect as MULTI_USER_BASIC due to priority
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.MULTI_USER_BASIC
|
assert mode == AuthMode.MULTI_USER_BASIC
|
||||||
# It will fail multi-user validation because username/password are forbidden
|
# Should report errors for forbidden username/password
|
||||||
assert len(errors) > 0
|
assert len(errors) > 0
|
||||||
|
|
||||||
def test_vector_sync_without_embedding_provider_uses_fallback(self):
|
def test_vector_sync_without_embedding_provider_uses_fallback(self):
|
||||||
@@ -167,7 +181,7 @@ class TestMultiUserBasicValidation:
|
|||||||
"""Test valid minimal multi-user BasicAuth config."""
|
"""Test valid minimal multi-user BasicAuth config."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
)
|
)
|
||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
@@ -179,7 +193,7 @@ class TestMultiUserBasicValidation:
|
|||||||
"""Test valid config with offline access enabled."""
|
"""Test valid config with offline access enabled."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
enable_offline_access=True,
|
enable_offline_access=True,
|
||||||
oidc_client_id="test-client",
|
oidc_client_id="test-client",
|
||||||
oidc_client_secret="test-secret",
|
oidc_client_secret="test-secret",
|
||||||
@@ -195,7 +209,7 @@ class TestMultiUserBasicValidation:
|
|||||||
def test_missing_required_host(self):
|
def test_missing_required_host(self):
|
||||||
"""Test error when NEXTCLOUD_HOST is missing."""
|
"""Test error when NEXTCLOUD_HOST is missing."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
enable_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
)
|
)
|
||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
@@ -209,13 +223,12 @@ class TestMultiUserBasicValidation:
|
|||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
nextcloud_username="admin",
|
nextcloud_username="admin",
|
||||||
nextcloud_password="password",
|
nextcloud_password="password",
|
||||||
enable_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
)
|
)
|
||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
# Multi-user BasicAuth has higher priority than single-user in detection
|
# Explicit MCP_DEPLOYMENT_MODE wins over auto-detection from credentials
|
||||||
# (explicit flags come before credentials)
|
|
||||||
assert mode == AuthMode.MULTI_USER_BASIC
|
assert mode == AuthMode.MULTI_USER_BASIC
|
||||||
# Should report errors for forbidden username/password
|
# Should report errors for forbidden username/password
|
||||||
assert any("nextcloud_username" in err.lower() for err in errors)
|
assert any("nextcloud_username" in err.lower() for err in errors)
|
||||||
@@ -225,7 +238,7 @@ class TestMultiUserBasicValidation:
|
|||||||
"""Test that offline access works without OAuth credentials (will use DCR)."""
|
"""Test that offline access works without OAuth credentials (will use DCR)."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
enable_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
enable_offline_access=True,
|
enable_offline_access=True,
|
||||||
token_encryption_key="test-key-" + "a" * 32,
|
token_encryption_key="test-key-" + "a" * 32,
|
||||||
token_storage_db="/tmp/tokens.db",
|
token_storage_db="/tmp/tokens.db",
|
||||||
@@ -241,7 +254,7 @@ class TestMultiUserBasicValidation:
|
|||||||
"""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_multi_user_basic_auth=True,
|
deployment_mode="multi_user_basic",
|
||||||
enable_offline_access=True,
|
enable_offline_access=True,
|
||||||
oidc_client_id="test-client",
|
oidc_client_id="test-client",
|
||||||
oidc_client_secret="test-secret",
|
oidc_client_secret="test-secret",
|
||||||
@@ -261,7 +274,7 @@ class TestMultiUserBasicValidation:
|
|||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NEXTCLOUD_HOST": "http://localhost:8080",
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
"ENABLE_MULTI_USER_BASIC_AUTH": "true",
|
"MCP_DEPLOYMENT_MODE": "multi_user_basic",
|
||||||
"VECTOR_SYNC_ENABLED": "true", # Using old name for backward compat test
|
"VECTOR_SYNC_ENABLED": "true", # Using old name for backward compat test
|
||||||
"QDRANT_LOCATION": ":memory:",
|
"QDRANT_LOCATION": ":memory:",
|
||||||
"OLLAMA_BASE_URL": "http://ollama:11434",
|
"OLLAMA_BASE_URL": "http://ollama:11434",
|
||||||
@@ -659,7 +672,7 @@ class TestConfigurationConsolidation:
|
|||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NEXTCLOUD_HOST": "http://localhost:8080",
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
"ENABLE_MULTI_USER_BASIC_AUTH": "true",
|
"MCP_DEPLOYMENT_MODE": "multi_user_basic",
|
||||||
"ENABLE_SEMANTIC_SEARCH": "true",
|
"ENABLE_SEMANTIC_SEARCH": "true",
|
||||||
"QDRANT_LOCATION": ":memory:",
|
"QDRANT_LOCATION": ":memory:",
|
||||||
"TOKEN_ENCRYPTION_KEY": "test-key",
|
"TOKEN_ENCRYPTION_KEY": "test-key",
|
||||||
@@ -830,3 +843,54 @@ class TestExplicitModeSelection:
|
|||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.LOGIN_FLOW
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
|
def test_legacy_enable_multi_user_basic_auth_env_var_errors(self):
|
||||||
|
"""ADR-022 follow-up: ENABLE_MULTI_USER_BASIC_AUTH=true must fail loudly.
|
||||||
|
|
||||||
|
The env-var alias was removed; users must migrate to
|
||||||
|
`MCP_DEPLOYMENT_MODE=multi_user_basic`. Silent removal would have
|
||||||
|
switched users to LOGIN_FLOW (the default) — wrong runtime mode.
|
||||||
|
"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
|
"ENABLE_MULTI_USER_BASIC_AUTH": "true",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
from nextcloud_mcp_server.config import get_settings
|
||||||
|
|
||||||
|
_reload_config()
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
detect_auth_mode(settings)
|
||||||
|
|
||||||
|
assert "ENABLE_MULTI_USER_BASIC_AUTH" in str(exc.value)
|
||||||
|
assert "multi_user_basic" in str(exc.value)
|
||||||
|
|
||||||
|
def test_legacy_enable_login_flow_env_var_errors(self):
|
||||||
|
"""ADR-022 follow-up: ENABLE_LOGIN_FLOW=true must fail loudly.
|
||||||
|
|
||||||
|
Mirrors the ENABLE_MULTI_USER_BASIC_AUTH check — both legacy aliases
|
||||||
|
now error with a one-line migration message.
|
||||||
|
"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
|
"ENABLE_LOGIN_FLOW": "true",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
from nextcloud_mcp_server.config import get_settings
|
||||||
|
|
||||||
|
_reload_config()
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
detect_auth_mode(settings)
|
||||||
|
|
||||||
|
assert "ENABLE_LOGIN_FLOW" in str(exc.value)
|
||||||
|
assert "login_flow" in str(exc.value)
|
||||||
|
|||||||
Reference in New Issue
Block a user