fix(vector): self-heal stale app passwords on auth failure
Deleted/disabled Nextcloud users left their app_passwords row in storage, so user_manager_task re-spawned their scanner every poll interval only to 401 again — an endless re-spawn/auth-failure loop (observed on tenant-blackbox-demo: ~534 respawns/3h, matching the 60s poll interval). - Delete the stored app password on a hard 401/403 in user_scanner_task (both the pre-validation and in-scan-loop paths), breaking the re-spawn loop at the source so the user-manager stops recreating the scanner. - Add a periodic credential_cleanup_task backstop (hourly) that sweeps cleanup_invalid_app_passwords for anything the per-scanner path misses. - Run the startup cleanup for all deployment modes: drop the stale `not oauth_enabled` guard so login_flow tenants (the cloud default) are covered. NOTE: login_flow startup now makes one concurrent OCS validation call per stored user before readiness. Refs Deck #198. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
60df141442
commit
3790cf6d60
+29
-13
@@ -130,6 +130,7 @@ from nextcloud_mcp_server.server.oauth_tools import register_oauth_tools
|
||||
from nextcloud_mcp_server.vector.metrics_publisher import vector_sync_metrics_task
|
||||
from nextcloud_mcp_server.vector.oauth_sync import (
|
||||
ProvisionSignal,
|
||||
credential_cleanup_task,
|
||||
oauth_processor_task,
|
||||
user_manager_task,
|
||||
)
|
||||
@@ -2067,20 +2068,25 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
||||
# how many per-user scanners the user-manager later starts.
|
||||
await _sweep_orphan_placeholders_if_enabled()
|
||||
|
||||
# Clean up stale app passwords at startup (BasicAuth mode only)
|
||||
if not oauth_enabled:
|
||||
try:
|
||||
removed = await token_storage.cleanup_invalid_app_passwords(
|
||||
nextcloud_host=nextcloud_host_for_sync
|
||||
# Clean up stale app passwords at startup. All deployment modes
|
||||
# now authenticate background sync via locally-stored app
|
||||
# passwords (the OAuth refresh-token path was removed), so this
|
||||
# must run regardless of oauth_enabled — login_flow tenants were
|
||||
# previously skipped, letting deleted-user credentials linger and
|
||||
# drive an endless scanner re-spawn/401 loop (Deck #198). The
|
||||
# credential_cleanup_task started below repeats it on a cadence.
|
||||
try:
|
||||
removed = await token_storage.cleanup_invalid_app_passwords(
|
||||
nextcloud_host=nextcloud_host_for_sync
|
||||
)
|
||||
if removed:
|
||||
logger.info(
|
||||
"Cleaned up %s stale app password(s): %s",
|
||||
len(removed),
|
||||
removed,
|
||||
)
|
||||
if removed:
|
||||
logger.info(
|
||||
"Cleaned up %s stale app password(s): %s",
|
||||
len(removed),
|
||||
removed,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("App password cleanup failed (non-fatal): %s", e)
|
||||
except Exception as e:
|
||||
logger.warning("App password cleanup failed (non-fatal): %s", e)
|
||||
|
||||
# Initialize the ingest transport. INGEST_QUEUE selects the
|
||||
# backend (Deck #183, ADR-028): ``memory`` builds an in-process
|
||||
@@ -2134,6 +2140,16 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
||||
provision_signal,
|
||||
)
|
||||
|
||||
# Periodic backstop sweep removing app passwords that no
|
||||
# longer authenticate (deleted/disabled users), complementing
|
||||
# the per-scanner self-heal in user_scanner_task (Deck #198).
|
||||
await tg.start(
|
||||
credential_cleanup_task,
|
||||
token_storage,
|
||||
shutdown_event,
|
||||
nextcloud_host_for_sync,
|
||||
)
|
||||
|
||||
# In-process consumer pool. ``run_consumers`` is a no-op for
|
||||
# the distributed (postgres) backend — the out-of-process
|
||||
# ``worker`` role consumes there. The closure binds this
|
||||
|
||||
Reference in New Issue
Block a user