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
@@ -97,3 +97,48 @@ def test_password_takes_precedence_over_token(mocker):
|
||||
call_kwargs = mock_dav_client.call_args.kwargs
|
||||
assert call_kwargs["password"] == "app-pw"
|
||||
assert call_kwargs["auth_type"] == "basic"
|
||||
|
||||
|
||||
def test_auth_username_used_for_credential_uid_for_path(mocker):
|
||||
"""OIDC users: the loginName authenticates, the UID builds the DAV path.
|
||||
|
||||
Nextcloud keys app-password auth on the loginName (which can differ from
|
||||
the UID), but ``/remote.php/dav/calendars/<uid>/`` must use the UID. The
|
||||
two must not be conflated.
|
||||
"""
|
||||
mock_dav_client = mocker.patch(
|
||||
"nextcloud_mcp_server.client.calendar.AsyncDAVClient"
|
||||
)
|
||||
|
||||
from nextcloud_mcp_server.client.calendar import CalendarClient
|
||||
|
||||
client = CalendarClient(
|
||||
"https://cloud.example.org",
|
||||
"Ada Lovelace", # UID
|
||||
auth_username="ada@example.com", # loginName
|
||||
password="app-pw-1234",
|
||||
)
|
||||
|
||||
# Credential identity → loginName
|
||||
assert mock_dav_client.call_args.kwargs["username"] == "ada@example.com"
|
||||
# Path identity → UID
|
||||
assert client.username == "Ada Lovelace"
|
||||
assert (
|
||||
client._calendar_home_url
|
||||
== "https://cloud.example.org/remote.php/dav/calendars/Ada Lovelace/"
|
||||
)
|
||||
|
||||
|
||||
def test_auth_username_defaults_to_username(mocker):
|
||||
"""Backwards compat: without ``auth_username`` the UID is used for both,
|
||||
so single-user / OAuth callers (UID == loginName) are unchanged.
|
||||
"""
|
||||
mock_dav_client = mocker.patch(
|
||||
"nextcloud_mcp_server.client.calendar.AsyncDAVClient"
|
||||
)
|
||||
|
||||
from nextcloud_mcp_server.client.calendar import CalendarClient
|
||||
|
||||
CalendarClient("https://cloud.example.org", "alice", password="app-pw")
|
||||
|
||||
assert mock_dav_client.call_args.kwargs["username"] == "alice"
|
||||
|
||||
Reference in New Issue
Block a user