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
@@ -257,7 +257,7 @@ async def test_provision_app_password_success(temp_storage, mocker):
|
||||
async def test_provision_app_password_uses_loginname_not_uid(temp_storage, mocker):
|
||||
"""Regression: when the Nextcloud UID differs from the loginName (e.g.
|
||||
OIDC-provisioned users whose UID is their display name — UID
|
||||
"Chris Coutinho", loginName "chris@coutinho.io"), the OCS BasicAuth
|
||||
"Ada Lovelace", loginName "ada@example.com"), the OCS BasicAuth
|
||||
validation must authenticate as the loginName from the request body, not
|
||||
the UID. Authenticating as the UID is rejected by Nextcloud with HTTP 401.
|
||||
"""
|
||||
@@ -273,7 +273,7 @@ async def test_provision_app_password_uses_loginname_not_uid(temp_storage, mocke
|
||||
# OCS validation succeeds and reports the UID as the account id.
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"ocs": {"data": {"id": "Chris Coutinho"}}}
|
||||
mock_response.json.return_value = {"ocs": {"data": {"id": "Ada Lovelace"}}}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get = AsyncMock(return_value=mock_response)
|
||||
@@ -291,9 +291,9 @@ async def test_provision_app_password_uses_loginname_not_uid(temp_storage, mocke
|
||||
# A literal space in the path is encoded by the client and decoded back to
|
||||
# the UID; the BasicAuth username matches that UID.
|
||||
response = client.post(
|
||||
"/api/v1/users/Chris Coutinho/app-password",
|
||||
headers={"Authorization": create_basic_auth_header("Chris Coutinho", pw)},
|
||||
json={"username": "chris@coutinho.io"},
|
||||
"/api/v1/users/Ada Lovelace/app-password",
|
||||
headers={"Authorization": create_basic_auth_header("Ada Lovelace", pw)},
|
||||
json={"username": "ada@example.com"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -301,10 +301,10 @@ async def test_provision_app_password_uses_loginname_not_uid(temp_storage, mocke
|
||||
|
||||
# The OCS BasicAuth used the loginName from the body, not the UID.
|
||||
_, get_kwargs = mock_client.get.call_args
|
||||
assert get_kwargs["auth"] == ("chris@coutinho.io", pw)
|
||||
assert get_kwargs["auth"] == ("ada@example.com", pw)
|
||||
|
||||
# Stored under the UID (the identity key).
|
||||
assert await temp_storage.get_app_password("Chris Coutinho") == pw
|
||||
assert await temp_storage.get_app_password("Ada Lovelace") == pw
|
||||
|
||||
|
||||
async def test_provision_app_password_nextcloud_validation_fails(mocker):
|
||||
|
||||
Reference in New Issue
Block a user