fix(auth): authenticate stored app passwords with loginName, not UID
Nextcloud authenticates app passwords against the *loginName*, which differs from the UID for OIDC-provisioned users (e.g. user_oidc makes the UID the display name: UID "Ada Lovelace", loginName "ada@example.com"). The runtime consumers of stored app passwords bound the UID as the BasicAuth username, so every Notes/Files/Shares/CalDAV call returned HTTP 401. PR #818 fixed only the provisioning endpoint; the consuming paths were missed. Observed on a login_flow tenant (NC's own OIDC app as IdP): the background-sync scan loop never started ("Credential validation failed ... HTTP 401") and semantic search returned 0 results because the ACL shared_with_me lookup 401'd and degraded to a self-only owner filter. Root cause: NextcloudClient / CalendarClient conflated two identities — the DAV/URL path identity (the user_id the whole system keys on = NC UID) and the auth-credential username (the loginName). Decouple them: - Thread a keyword-only auth_username through NextcloudClient -> CalendarClient (defaults to username, so single-user / OAuth where UID == loginName is unchanged). - get_user_client_basic_auth (background sync + the /api/v1/vector-viz/search endpoint) authenticates as the stored loginName, UID for paths. - _get_client_from_login_flow (the get_client(ctx) MCP-tool path) does the same. - cleanup_invalid_app_passwords validates with the loginName, so it no longer 401s and wrongly deletes a valid OIDC user's password. The loginName is already persisted in app_passwords.username and returned by get_app_password_with_scopes. Adds unit tests covering the UID != loginName split for both client builders, the calendar credential/path split, and the cleanup validation. Also genericises the example user in the #818 comment/test (real name/email -> Ada Lovelace / ada@example.com). 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
e7512d210c
commit
52da297ded
@@ -102,10 +102,21 @@ class NextcloudClient:
|
||||
username: str,
|
||||
auth: Auth | None = None,
|
||||
*,
|
||||
auth_username: str | None = None,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
):
|
||||
# ``username`` is the Nextcloud UID — it drives DAV/API path
|
||||
# construction (e.g. ``/remote.php/dav/files/<uid>/``). ``auth_username``
|
||||
# is the credential identity Nextcloud authenticates the app password
|
||||
# against (the loginName), which differs from the UID for
|
||||
# OIDC-provisioned users. Defaults to ``username`` so single-user and
|
||||
# OAuth modes (where UID == loginName) are unchanged. Callers pass the
|
||||
# matching ``auth=BasicAuth(auth_username, ...)`` for the httpx leg;
|
||||
# ``auth_username`` is threaded to the CalDAV client, which builds its
|
||||
# own auth object from the raw credential.
|
||||
self.username = username
|
||||
auth_username = auth_username or username
|
||||
self._client = AsyncClient(
|
||||
base_url=base_url,
|
||||
auth=auth,
|
||||
@@ -122,7 +133,11 @@ class NextcloudClient:
|
||||
# its preferred backend in v3.x) builds a backend-compatible auth object
|
||||
# itself — passing httpx.BasicAuth here breaks under niquests (#731).
|
||||
self.calendar = CalendarClient(
|
||||
base_url, username, password=password, token=token
|
||||
base_url,
|
||||
username,
|
||||
auth_username=auth_username,
|
||||
password=password,
|
||||
token=token,
|
||||
)
|
||||
self.contacts = ContactsClient(self._client, username)
|
||||
self.cookbook = CookbookClient(self._client, username)
|
||||
|
||||
@@ -42,6 +42,7 @@ class CalendarClient:
|
||||
base_url: str,
|
||||
username: str,
|
||||
*,
|
||||
auth_username: str | None = None,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
):
|
||||
@@ -55,7 +56,10 @@ class CalendarClient:
|
||||
|
||||
Args:
|
||||
base_url: Nextcloud base URL
|
||||
username: Nextcloud username
|
||||
username: Nextcloud username (UID) — used for DAV path construction
|
||||
auth_username: Credential identity (loginName) the app password
|
||||
authenticates against; defaults to ``username``. Differs from
|
||||
the UID for OIDC-provisioned users.
|
||||
password: App password / login password — selects ``auth_type="basic"``
|
||||
token: OAuth bearer token — selects ``auth_type="bearer"``
|
||||
|
||||
@@ -64,6 +68,11 @@ class CalendarClient:
|
||||
"""
|
||||
self.username = username
|
||||
self.base_url = base_url
|
||||
# The UID (``username``) drives DAV path construction; the loginName
|
||||
# (``auth_username``) is the credential the app password authenticates
|
||||
# against. They differ for OIDC-provisioned users. Defaults to the UID
|
||||
# so existing single-user / OAuth callers are unchanged.
|
||||
auth_username = auth_username or username
|
||||
|
||||
auth_kwargs: dict[str, Any] = {}
|
||||
if password is not None:
|
||||
@@ -74,7 +83,7 @@ class CalendarClient:
|
||||
# AsyncDAVClient needs the full base URL for proper URL construction
|
||||
self._dav_client = AsyncDAVClient(
|
||||
url=f"{base_url}/remote.php/dav/",
|
||||
username=username,
|
||||
username=auth_username,
|
||||
ssl_verify_cert=get_nextcloud_ssl_verify(), # type: ignore[arg-type] # caldav types say bool|str but passes through to niquests which accepts SSLContext
|
||||
**auth_kwargs,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user