refactor(config)!: rename OAUTH_SINGLE_AUDIENCE to LOGIN_FLOW, gate on ENABLE_LOGIN_FLOW
The AuthMode.OAUTH_SINGLE_AUDIENCE enum was a vestige of ADR-021's
original design where it co-existed with OAUTH_TOKEN_EXCHANGE. The
un-augmented OAuth bearer pass-through it represented relied on
Nextcloud-side patches to user_oidc (Bearer token validation on
non-OCS endpoints) that were never merged upstream (see
docs/authentication.md, docs/login-flow-v2.md). The working path —
mcp-login-flow profile — sets ENABLE_LOGIN_FLOW=true on top of this
mode so Login Flow v2 acquires per-user Nextcloud app passwords via
a browser flow. With OAUTH_TOKEN_EXCHANGE removed in 57303135, the
_AUDIENCE suffix in the Python name no longer disambiguates anything,
and the enum value diverged from the env-var spelling. ADR-022 (now
accepted) called for this rename as step 1 of consolidation.
- nextcloud_mcp_server/config_validators.py: rename enum to LOGIN_FLOW
with value "login_flow". The mode_map key is now "login_flow"; the
MODE_REQUIREMENTS entry requires `enable_login_flow=True`. Added a
validation gate so MCP_DEPLOYMENT_MODE=login_flow without
ENABLE_LOGIN_FLOW=true errors with a clear message pointing at
ADR-022. Default auto-detection fallback returns LOGIN_FLOW.
- nextcloud_mcp_server/app.py: renamed three identifier references and
switched the "Configuring MCP server for OAuth mode" log line to
the uniform `mode.value` shape used by the other modes.
- nextcloud_mcp_server/api/management.py: renamed identifier in the
/api/v1/status mapping. The user-visible "auth_mode": "oauth" string
is preserved — that's a stable Astrolabe contract.
- nextcloud_mcp_server/config.py: updated Settings docstring.
- tests/unit/test_config_validators.py: renamed class
TestOAuthSingleAudienceValidation → TestLoginFlowValidation,
individual test methods, env-var strings; added enable_login_flow=True
to fixtures expecting success; added a new test
(test_login_flow_requires_enable_login_flow_flag) that exercises the
validation gate.
- tests/unit/test_management_status_endpoint.py: renamed identifier.
BREAKING CHANGE: MCP_DEPLOYMENT_MODE=oauth_single_audience is no longer
accepted. Set MCP_DEPLOYMENT_MODE=login_flow (and keep
ENABLE_LOGIN_FLOW=true) for the same deployment. The un-augmented
OAuth path is no longer supported; if you previously ran the broken
path, you can either configure Login Flow v2 (recommended) or switch
to multi_user_basic / single_user_basic.
Dead-code pruning of `oauth_enabled and not enable_login_flow`
branches in app.py (lifespan, background sync) is deferred to a
separate follow-up PR per the consolidation plan in ADR-022.
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
586859d66a
commit
cafd318f36
@@ -212,7 +212,7 @@ async def get_server_status(request: Request) -> JSONResponse:
|
|||||||
|
|
||||||
# Map deployment mode to auth_mode for API response
|
# Map deployment mode to auth_mode for API response
|
||||||
# This helps clients (like Astrolabe) determine which auth flow to use
|
# This helps clients (like Astrolabe) determine which auth flow to use
|
||||||
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
|
if mode == AuthMode.LOGIN_FLOW:
|
||||||
auth_mode = "oauth"
|
auth_mode = "oauth"
|
||||||
elif mode == AuthMode.MULTI_USER_BASIC:
|
elif mode == AuthMode.MULTI_USER_BASIC:
|
||||||
auth_mode = "multi_user_basic"
|
auth_mode = "multi_user_basic"
|
||||||
|
|||||||
@@ -1015,8 +1015,11 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
logger.info(f"✅ Configuration validated successfully for {mode.value} mode")
|
logger.info(f"✅ Configuration validated successfully for {mode.value} mode")
|
||||||
logger.debug(f"Mode details:\n{get_mode_summary(mode)}")
|
logger.debug(f"Mode details:\n{get_mode_summary(mode)}")
|
||||||
|
|
||||||
# Derive helper variables for backward compatibility with existing code
|
# Derive helper variables for backward compatibility with existing code.
|
||||||
oauth_enabled = mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
# `oauth_enabled` is True for the LOGIN_FLOW (formerly OAUTH_SINGLE_AUDIENCE)
|
||||||
|
# multi-user OAuth mode — in this mode the MCP server is an OIDC relying
|
||||||
|
# party and Login Flow v2 acquires per-user Nextcloud app passwords.
|
||||||
|
oauth_enabled = mode == AuthMode.LOGIN_FLOW
|
||||||
# Log hybrid authentication status for multi-user BasicAuth with offline access
|
# Log hybrid authentication status for multi-user BasicAuth with offline access
|
||||||
if mode == AuthMode.MULTI_USER_BASIC and settings.enable_offline_access:
|
if mode == AuthMode.MULTI_USER_BASIC and settings.enable_offline_access:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -1166,8 +1169,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
# Create MCP server based on detected mode
|
# Create MCP server based on detected mode
|
||||||
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
|
if mode == AuthMode.LOGIN_FLOW:
|
||||||
logger.info("Configuring MCP server for OAuth mode")
|
logger.info("Configuring MCP server for %s mode", mode.value)
|
||||||
# Asynchronously get the OAuth configuration
|
# Asynchronously get the OAuth configuration
|
||||||
|
|
||||||
(
|
(
|
||||||
@@ -1932,7 +1935,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
# Check authentication configuration
|
# Check authentication configuration
|
||||||
# Report the deployment mode, not just whether OAuth is enabled
|
# Report the deployment mode, not just whether OAuth is enabled
|
||||||
# This helps clients (like Astrolabe) determine which auth flow to use
|
# This helps clients (like Astrolabe) determine which auth flow to use
|
||||||
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
|
if mode == AuthMode.LOGIN_FLOW:
|
||||||
checks["auth_mode"] = "oauth"
|
checks["auth_mode"] = "oauth"
|
||||||
checks["auth_configured"] = "ok"
|
checks["auth_configured"] = "ok"
|
||||||
elif mode == AuthMode.MULTI_USER_BASIC:
|
elif mode == AuthMode.MULTI_USER_BASIC:
|
||||||
|
|||||||
@@ -408,9 +408,10 @@ def get_document_processor_config() -> dict[str, Any]:
|
|||||||
class Settings:
|
class Settings:
|
||||||
"""Application settings from environment variables."""
|
"""Application settings from environment variables."""
|
||||||
|
|
||||||
# Deployment mode (ADR-021: explicit mode selection)
|
# Deployment mode (ADR-021: explicit mode selection; updated by ADR-022)
|
||||||
# Optional: If not set, mode is auto-detected from other settings
|
# Optional: If not set, mode is auto-detected from other settings
|
||||||
# Valid values: single_user_basic, multi_user_basic, oauth_single_audience
|
# Valid values: single_user_basic, multi_user_basic, login_flow
|
||||||
|
# (ADR-022: `oauth_single_audience` was renamed to `login_flow`.)
|
||||||
deployment_mode: str | None = None
|
deployment_mode: str | None = None
|
||||||
|
|
||||||
# OAuth/OIDC settings
|
# OAuth/OIDC settings
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class AuthMode(Enum):
|
|||||||
|
|
||||||
SINGLE_USER_BASIC = "single_user_basic"
|
SINGLE_USER_BASIC = "single_user_basic"
|
||||||
MULTI_USER_BASIC = "multi_user_basic"
|
MULTI_USER_BASIC = "multi_user_basic"
|
||||||
OAUTH_SINGLE_AUDIENCE = "oauth_single"
|
LOGIN_FLOW = "login_flow"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -113,8 +113,8 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
|
|||||||
"Users provide credentials in request headers. "
|
"Users provide credentials in request headers. "
|
||||||
"Optional background sync using app passwords stored via Astrolabe.",
|
"Optional background sync using app passwords stored via Astrolabe.",
|
||||||
),
|
),
|
||||||
AuthMode.OAUTH_SINGLE_AUDIENCE: ModeRequirements(
|
AuthMode.LOGIN_FLOW: ModeRequirements(
|
||||||
required=["nextcloud_host"],
|
required=["nextcloud_host", "enable_login_flow"],
|
||||||
optional=[
|
optional=[
|
||||||
# OAuth credentials (uses DCR if not provided)
|
# OAuth credentials (uses DCR if not provided)
|
||||||
"oidc_client_id",
|
"oidc_client_id",
|
||||||
@@ -149,9 +149,13 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
|
|||||||
# enables background operations in multi-user modes. No explicit
|
# enables background operations in multi-user modes. No explicit
|
||||||
# enable_offline_access setting required.
|
# enable_offline_access setting required.
|
||||||
},
|
},
|
||||||
description="OAuth multi-user deployment with single-audience tokens. "
|
description="OAuth multi-user deployment using Login Flow v2 to acquire "
|
||||||
"Tokens work for both MCP server and Nextcloud APIs (pass-through). "
|
"per-user Nextcloud app passwords via a browser flow. The MCP server "
|
||||||
"Uses Dynamic Client Registration if credentials not provided.",
|
"is an OIDC relying party of a configurable IdP (Nextcloud's built-in "
|
||||||
|
"OIDC by default; Keycloak, AWS Cognito, etc. via OIDC_DISCOVERY_URL). "
|
||||||
|
"Uses Dynamic Client Registration if credentials not provided. "
|
||||||
|
"Replaces the deprecated direct OAuth bearer-token pass-through which "
|
||||||
|
"required unmerged user_oidc patches (see ADR-022).",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,11 +163,11 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
|
|||||||
def detect_auth_mode(settings: Settings) -> AuthMode:
|
def detect_auth_mode(settings: Settings) -> AuthMode:
|
||||||
"""Detect authentication mode from configuration.
|
"""Detect authentication mode from configuration.
|
||||||
|
|
||||||
Mode detection priority (ADR-021):
|
Mode detection priority (ADR-021, updated for ADR-022):
|
||||||
0. Explicit MCP_DEPLOYMENT_MODE (if set) - NEW in ADR-021
|
0. Explicit MCP_DEPLOYMENT_MODE (if set) - NEW in ADR-021
|
||||||
1. Multi-user BasicAuth
|
1. Multi-user BasicAuth
|
||||||
2. Single-user BasicAuth
|
2. Single-user BasicAuth
|
||||||
3. OAuth single-audience (default OAuth mode)
|
3. Login Flow v2 (default — was OAuth single-audience pre-ADR-022)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings: Application settings
|
settings: Application settings
|
||||||
@@ -185,7 +189,7 @@ def detect_auth_mode(settings: Settings) -> AuthMode:
|
|||||||
mode_map = {
|
mode_map = {
|
||||||
"single_user_basic": AuthMode.SINGLE_USER_BASIC,
|
"single_user_basic": AuthMode.SINGLE_USER_BASIC,
|
||||||
"multi_user_basic": AuthMode.MULTI_USER_BASIC,
|
"multi_user_basic": AuthMode.MULTI_USER_BASIC,
|
||||||
"oauth_single_audience": AuthMode.OAUTH_SINGLE_AUDIENCE,
|
"login_flow": AuthMode.LOGIN_FLOW,
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode_str not in mode_map:
|
if mode_str not in mode_map:
|
||||||
@@ -208,9 +212,11 @@ def detect_auth_mode(settings: Settings) -> AuthMode:
|
|||||||
if settings.nextcloud_username and settings.nextcloud_password:
|
if settings.nextcloud_username and settings.nextcloud_password:
|
||||||
return AuthMode.SINGLE_USER_BASIC
|
return AuthMode.SINGLE_USER_BASIC
|
||||||
|
|
||||||
# Default: OAuth single-audience mode
|
# Default: Login Flow v2 multi-user mode (browser-based app-password flow).
|
||||||
# This is the safest multi-user mode (no credential storage)
|
# The un-augmented OAuth bearer pass-through it replaced required unmerged
|
||||||
return AuthMode.OAUTH_SINGLE_AUDIENCE
|
# Nextcloud user_oidc patches (see ADR-022); selecting LOGIN_FLOW without
|
||||||
|
# ENABLE_LOGIN_FLOW=true fails validation below.
|
||||||
|
return AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
|
|
||||||
def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
||||||
@@ -301,7 +307,18 @@ def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
|
|||||||
f"{settings.nextcloud_host}"
|
f"{settings.nextcloud_host}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
|
if mode == AuthMode.LOGIN_FLOW:
|
||||||
|
# ADR-022: LOGIN_FLOW requires the Login Flow v2 layer. The un-augmented
|
||||||
|
# OAuth bearer pass-through (formerly OAUTH_SINGLE_AUDIENCE without
|
||||||
|
# ENABLE_LOGIN_FLOW) needed unmerged Nextcloud user_oidc patches and
|
||||||
|
# is no longer supported.
|
||||||
|
if not settings.enable_login_flow:
|
||||||
|
errors.append(
|
||||||
|
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)
|
||||||
if not settings.oidc_client_id or not settings.oidc_client_secret:
|
if not settings.oidc_client_id or not settings.oidc_client_secret:
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ class TestModeDetection:
|
|||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
assert mode == AuthMode.SINGLE_USER_BASIC
|
assert mode == AuthMode.SINGLE_USER_BASIC
|
||||||
|
|
||||||
def test_oauth_single_audience_default(self):
|
def test_login_flow_default(self):
|
||||||
"""Test OAuth single-audience is default mode."""
|
"""Test Login Flow v2 is the default multi-user mode."""
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
nextcloud_host="http://localhost",
|
nextcloud_host="http://localhost",
|
||||||
)
|
)
|
||||||
|
|
||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
|
|
||||||
class TestSingleUserBasicValidation:
|
class TestSingleUserBasicValidation:
|
||||||
@@ -108,7 +108,7 @@ class TestSingleUserBasicValidation:
|
|||||||
|
|
||||||
# Mode detection requires BOTH username AND password for single-user BasicAuth
|
# Mode detection requires BOTH username AND password for single-user BasicAuth
|
||||||
# If only one is present, it defaults to OAuth single-audience
|
# If only one is present, it defaults to OAuth single-audience
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
# In OAuth mode, having a password set is forbidden
|
# In OAuth mode, having a password set is forbidden
|
||||||
assert any("nextcloud_password" in err.lower() for err in errors)
|
assert any("nextcloud_password" in err.lower() for err in errors)
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class TestSingleUserBasicValidation:
|
|||||||
|
|
||||||
# Mode detection requires BOTH username AND password for single-user BasicAuth
|
# Mode detection requires BOTH username AND password for single-user BasicAuth
|
||||||
# If only one is present, it defaults to OAuth single-audience
|
# If only one is present, it defaults to OAuth single-audience
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
# 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)
|
||||||
|
|
||||||
@@ -285,37 +285,40 @@ class TestMultiUserBasicValidation:
|
|||||||
assert settings.enable_offline_access is True
|
assert settings.enable_offline_access is True
|
||||||
|
|
||||||
|
|
||||||
class TestOAuthSingleAudienceValidation:
|
class TestLoginFlowValidation:
|
||||||
"""Test validation for OAuth single-audience mode."""
|
"""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 OAuth single-audience config."""
|
"""Test valid minimal Login Flow v2 config."""
|
||||||
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.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
assert len(errors) == 0
|
assert len(errors) == 0
|
||||||
|
|
||||||
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",
|
||||||
)
|
)
|
||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
assert len(errors) == 0
|
assert len(errors) == 0
|
||||||
|
|
||||||
def test_valid_with_offline_access(self):
|
def test_valid_with_offline_access(self):
|
||||||
"""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,
|
||||||
@@ -325,7 +328,7 @@ class TestOAuthSingleAudienceValidation:
|
|||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
assert len(errors) == 0
|
assert len(errors) == 0
|
||||||
|
|
||||||
def test_forbidden_username_password(self):
|
def test_forbidden_username_password(self):
|
||||||
@@ -345,29 +348,45 @@ class TestOAuthSingleAudienceValidation:
|
|||||||
"""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",
|
||||||
)
|
)
|
||||||
|
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
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_vector_sync_auto_enables_background_ops_in_oauth_mode(self):
|
def test_login_flow_requires_enable_login_flow_flag(self):
|
||||||
"""Test vector sync automatically enables background operations in OAuth mode (ADR-021)."""
|
"""ADR-022: LOGIN_FLOW mode must error when ENABLE_LOGIN_FLOW is not true."""
|
||||||
|
settings = Settings(
|
||||||
|
nextcloud_host="http://localhost",
|
||||||
|
# enable_login_flow deliberately omitted (defaults to False)
|
||||||
|
)
|
||||||
|
|
||||||
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
|
assert any("ENABLE_LOGIN_FLOW" in err for err in errors), (
|
||||||
|
f"Expected ENABLE_LOGIN_FLOW gate error, got: {errors}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)."""
|
||||||
# Before ADR-021: This would have failed validation (required explicit ENABLE_OFFLINE_ACCESS)
|
# Before ADR-021: This would have failed validation (required explicit ENABLE_OFFLINE_ACCESS)
|
||||||
# After ADR-021: vector_sync_enabled auto-enables background operations in multi-user modes
|
# After ADR-021: vector_sync_enabled auto-enables background operations in multi-user modes
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
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",
|
||||||
"TOKEN_ENCRYPTION_KEY": "test-key",
|
"TOKEN_ENCRYPTION_KEY": "test-key",
|
||||||
"TOKEN_STORAGE_DB": "/tmp/test.db",
|
"TOKEN_STORAGE_DB": "/tmp/test.db",
|
||||||
# Note: No username/password = OAuth mode
|
# Note: No username/password = Login Flow v2 multi-user OAuth mode
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
@@ -377,7 +396,7 @@ class TestOAuthSingleAudienceValidation:
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
mode, errors = validate_configuration(settings)
|
mode, errors = validate_configuration(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
# Should have no errors - background operations auto-enabled
|
# Should have no errors - background operations auto-enabled
|
||||||
assert len(errors) == 0
|
assert len(errors) == 0
|
||||||
# Verify background operations were auto-enabled
|
# Verify background operations were auto-enabled
|
||||||
@@ -515,8 +534,8 @@ class TestConfigurationConsolidation:
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
assert settings.enable_offline_access is True
|
assert settings.enable_offline_access is True
|
||||||
|
|
||||||
def test_semantic_search_auto_enables_background_ops_in_oauth_mode(self):
|
def test_semantic_search_auto_enables_background_ops_in_login_flow_mode(self):
|
||||||
"""Test ENABLE_SEMANTIC_SEARCH automatically enables background operations in OAuth mode."""
|
"""Test ENABLE_SEMANTIC_SEARCH automatically enables background operations in Login Flow v2 mode."""
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
@@ -707,13 +726,13 @@ class TestExplicitModeSelection:
|
|||||||
|
|
||||||
assert mode == AuthMode.MULTI_USER_BASIC
|
assert mode == AuthMode.MULTI_USER_BASIC
|
||||||
|
|
||||||
def test_explicit_oauth_single_audience_mode(self):
|
def test_explicit_login_flow_mode(self):
|
||||||
"""Test explicit oauth_single_audience mode selection."""
|
"""Test explicit login_flow mode selection."""
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NEXTCLOUD_HOST": "http://localhost:8080",
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
"MCP_DEPLOYMENT_MODE": "oauth_single_audience",
|
"MCP_DEPLOYMENT_MODE": "login_flow",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
@@ -723,7 +742,7 @@ class TestExplicitModeSelection:
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
def test_invalid_deployment_mode_raises_error(self):
|
def test_invalid_deployment_mode_raises_error(self):
|
||||||
"""Test invalid MCP_DEPLOYMENT_MODE raises ValueError."""
|
"""Test invalid MCP_DEPLOYMENT_MODE raises ValueError."""
|
||||||
@@ -757,7 +776,7 @@ class TestExplicitModeSelection:
|
|||||||
"NEXTCLOUD_HOST": "http://localhost:8080",
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
"NEXTCLOUD_USERNAME": "admin", # Would auto-detect as single_user_basic
|
"NEXTCLOUD_USERNAME": "admin", # Would auto-detect as single_user_basic
|
||||||
"NEXTCLOUD_PASSWORD": "password",
|
"NEXTCLOUD_PASSWORD": "password",
|
||||||
"MCP_DEPLOYMENT_MODE": "oauth_single_audience", # Explicit override
|
"MCP_DEPLOYMENT_MODE": "login_flow", # Explicit override
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
@@ -768,7 +787,7 @@ class TestExplicitModeSelection:
|
|||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
|
|
||||||
# Should use explicit mode, not auto-detected mode
|
# Should use explicit mode, not auto-detected mode
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
def test_case_insensitive_mode_names(self):
|
def test_case_insensitive_mode_names(self):
|
||||||
"""Test MCP_DEPLOYMENT_MODE is case-insensitive."""
|
"""Test MCP_DEPLOYMENT_MODE is case-insensitive."""
|
||||||
@@ -776,7 +795,7 @@ class TestExplicitModeSelection:
|
|||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NEXTCLOUD_HOST": "http://localhost:8080",
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
"MCP_DEPLOYMENT_MODE": "OAUTH_SINGLE_AUDIENCE", # Uppercase
|
"MCP_DEPLOYMENT_MODE": "LOGIN_FLOW", # Uppercase
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
@@ -786,7 +805,7 @@ class TestExplicitModeSelection:
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
|
|
||||||
def test_whitespace_in_mode_name_stripped(self):
|
def test_whitespace_in_mode_name_stripped(self):
|
||||||
"""Test whitespace in MCP_DEPLOYMENT_MODE is stripped."""
|
"""Test whitespace in MCP_DEPLOYMENT_MODE is stripped."""
|
||||||
@@ -794,7 +813,7 @@ class TestExplicitModeSelection:
|
|||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NEXTCLOUD_HOST": "http://localhost:8080",
|
"NEXTCLOUD_HOST": "http://localhost:8080",
|
||||||
"MCP_DEPLOYMENT_MODE": " oauth_single_audience ", # Whitespace
|
"MCP_DEPLOYMENT_MODE": " login_flow ", # Whitespace
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
@@ -804,4 +823,4 @@ class TestExplicitModeSelection:
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
mode = detect_auth_mode(settings)
|
mode = detect_auth_mode(settings)
|
||||||
|
|
||||||
assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE
|
assert mode == AuthMode.LOGIN_FLOW
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class TestStatusEndpointOidcConfig:
|
|||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"nextcloud_mcp_server.api.management.detect_auth_mode",
|
"nextcloud_mcp_server.api.management.detect_auth_mode",
|
||||||
return_value=AuthMode.OAUTH_SINGLE_AUDIENCE,
|
return_value=AuthMode.LOGIN_FLOW,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
app = create_test_app()
|
app = create_test_app()
|
||||||
|
|||||||
Reference in New Issue
Block a user