fix(auth): address PR #757 round-3 review feedback

Three review items from the third-round review on PR #757:

- scope_authorization: split the combined logger.warning(error_msg) in
  the require_scopes decorator's missing-app-password branch into two
  lazy %-style logger calls (one per branch), keeping the f-string
  error_msg for the exception only. The else branch also logs the
  elicit_result for diagnostics. Bypassing lazy %-interpolation in
  security-sensitive code formatted the message regardless of log level
  and matched the repo-wide lazy-logging preference; the new code now
  conforms.
- config + browser_oauth_routes: wire COOKIE_SECURE through Settings
  (cookie_secure: bool | None = None) so _should_use_secure_cookies()
  reads it via get_settings() rather than os.getenv. Completes the
  consolidation pass that touched this file in commit 7464340 and
  removes the last raw os.getenv from browser_oauth_routes.py
  (import os dropped). Dynaconf auto-coerces "true"/"false" → bool;
  "1"/"0" arrive as int and are normalised by an explicit bool() at
  the consumer.
- elicitation: clarify the _astrolabe_settings_url docstring to call
  out that the empty-string case is also a None-return path (matches
  the existing `if not base:` guard).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-02 17:24:50 +02:00
co-authored by Claude Opus 4.7
parent f31d0544b7
commit ce80a36877
4 changed files with 33 additions and 17 deletions
@@ -6,7 +6,6 @@ for accessing admin UI endpoints like /app.
import hashlib import hashlib
import logging import logging
import os
import secrets import secrets
import time import time
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
@@ -30,23 +29,21 @@ logger = logging.getLogger(__name__)
def _should_use_secure_cookies() -> bool: def _should_use_secure_cookies() -> bool:
"""Determine if cookies should have secure flag. """Determine if cookies should have the Secure flag.
Checks COOKIE_SECURE env var first, then auto-detects from NEXTCLOUD_HOST. Reads ``settings.cookie_secure`` first (set via the ``COOKIE_SECURE``
env var). Falls back to auto-detect from the ``nextcloud_host`` scheme
when unset.
Returns: Returns:
True if cookies should be secure (HTTPS), False otherwise True if cookies should be secure (HTTPS), False otherwise
""" """
# Explicit configuration takes precedence settings = get_settings()
explicit = os.getenv("COOKIE_SECURE", "").lower() if settings.cookie_secure is not None:
if explicit == "true": # Dynaconf auto-coerces "true"/"false" → bool but "1"/"0" → int;
return True # bool() normalises both.
if explicit == "false": return bool(settings.cookie_secure)
return False nextcloud_host = settings.nextcloud_host or ""
# Auto-detect from NEXTCLOUD_HOST protocol (read via Settings for
# consistency with the rest of this file).
nextcloud_host = get_settings().nextcloud_host or ""
return nextcloud_host.startswith("https://") return nextcloud_host.startswith("https://")
+4 -3
View File
@@ -46,9 +46,10 @@ def _astrolabe_settings_url() -> str | None:
Prefers ``nextcloud_public_issuer_url`` (the browser-reachable public URL) Prefers ``nextcloud_public_issuer_url`` (the browser-reachable public URL)
over ``nextcloud_host`` (which may be an internal hostname in Docker over ``nextcloud_host`` (which may be an internal hostname in Docker
deployments). Returns None if neither is set, or if the configured base deployments). Returns None if neither is set (or set to the empty
URL is missing an http:// or https:// scheme — in the latter case the string), or if the configured base URL is missing an http:// or
caller renders the tool-only fallback message instead of a broken link. https:// scheme — in the latter case the caller renders the tool-only
fallback message instead of a broken link.
""" """
settings = get_settings() settings = get_settings()
base = ( base = (
@@ -179,6 +179,11 @@ def require_scopes(*required_scopes: str):
# time the next retry can still hit a not-yet- # time the next retry can still hit a not-yet-
# populated entry — hence the "wait a moment" # populated entry — hence the "wait a moment"
# qualifier below. # qualifier below.
logger.warning(
"Access denied to %s: app password missing "
"after user accepted elicitation; advising retry",
func_name,
)
error_msg = ( error_msg = (
f"Access denied to {func_name}: Nextcloud " f"Access denied to {func_name}: Nextcloud "
f"access was not provisioned at the time of " f"access was not provisioned at the time of "
@@ -188,12 +193,18 @@ def require_scopes(*required_scopes: str):
f"completing; wait a moment and try again." f"completing; wait a moment and try again."
) )
else: else:
logger.warning(
"Access denied to %s: app password missing; "
"advising nc_auth_provision_access "
"(elicit_result=%s)",
func_name,
elicit_result,
)
error_msg = ( error_msg = (
f"Access denied to {func_name}: " f"Access denied to {func_name}: "
f"Nextcloud access not provisioned. " f"Nextcloud access not provisioned. "
f"Please call 'nc_auth_provision_access' first." f"Please call 'nc_auth_provision_access' first."
) )
logger.warning(error_msg)
raise ProvisioningRequiredError(error_msg) raise ProvisioningRequiredError(error_msg)
if stored_scopes == "all": if stored_scopes == "all":
+7
View File
@@ -32,6 +32,7 @@ _DEFAULTS: dict[str, Any] = {
"nextcloud_mcp_server_url": None, "nextcloud_mcp_server_url": None,
"nextcloud_resource_uri": None, "nextcloud_resource_uri": None,
"nextcloud_public_issuer_url": None, "nextcloud_public_issuer_url": None,
"cookie_secure": None,
# OAuth/OIDC # OAuth/OIDC
"oidc_discovery_url": None, "oidc_discovery_url": None,
"nextcloud_oidc_client_id": None, "nextcloud_oidc_client_id": None,
@@ -412,6 +413,11 @@ class Settings:
# nextcloud_host when unset. # nextcloud_host when unset.
nextcloud_public_issuer_url: str | None = None nextcloud_public_issuer_url: str | None = None
# Browser cookie Secure flag. None = auto-detect from nextcloud_host
# scheme (https → True, else False). Set COOKIE_SECURE=true/false to
# override.
cookie_secure: bool | None = None
# Nextcloud SSL/TLS settings # Nextcloud SSL/TLS settings
nextcloud_verify_ssl: bool = True nextcloud_verify_ssl: bool = True
nextcloud_ca_bundle: str | None = None nextcloud_ca_bundle: str | None = None
@@ -784,6 +790,7 @@ def get_settings() -> Settings:
"nextcloud_password": "NEXTCLOUD_PASSWORD", "nextcloud_password": "NEXTCLOUD_PASSWORD",
"nextcloud_app_password": "NEXTCLOUD_APP_PASSWORD", "nextcloud_app_password": "NEXTCLOUD_APP_PASSWORD",
"nextcloud_public_issuer_url": "NEXTCLOUD_PUBLIC_ISSUER_URL", "nextcloud_public_issuer_url": "NEXTCLOUD_PUBLIC_ISSUER_URL",
"cookie_secure": "COOKIE_SECURE",
# Nextcloud SSL/TLS settings # Nextcloud SSL/TLS settings
"nextcloud_verify_ssl": "NEXTCLOUD_VERIFY_SSL", "nextcloud_verify_ssl": "NEXTCLOUD_VERIFY_SSL",
"nextcloud_ca_bundle": "NEXTCLOUD_CA_BUNDLE", "nextcloud_ca_bundle": "NEXTCLOUD_CA_BUNDLE",