fix(calendar): thread raw credentials to caldav AsyncDAVClient
caldav 3.x lists niquests as a mandatory dependency and prefers it over httpx. Passing httpx.BasicAuth via the auth= argument breaks under the niquests backend with "Unexpected non-callable authentication" — see #731. Switch CalendarClient.__init__ from auth=Auth|None to keyword-only password/token, and forward them to AsyncDAVClient as password= plus an explicit auth_type ("basic" or "bearer"). caldav then builds whichever auth object its active backend needs (niquests.auth.HTTPBasicAuth or httpx.BasicAuth), so we stay backend-agnostic. Threaded raw credentials through NextcloudClient — added keyword-only password/token to its __init__, and updated from_env, from_token, and the four call sites that build NextcloudClient (context.py basic-auth and Login Flow paths, auth/userinfo_routes.py, vector/oauth_sync.py). Four new unit tests pin the construction wiring so the niquests regression can't recur silently — basic, bearer, no-creds, and password-precedence cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
4e669781ee
commit
2f1b0d2500
@@ -63,7 +63,15 @@ class AsyncDisableCookieTransport(AsyncBaseTransport):
|
||||
class NextcloudClient:
|
||||
"""Main Nextcloud client that orchestrates all app clients."""
|
||||
|
||||
def __init__(self, base_url: str, username: str, auth: Auth | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
username: str,
|
||||
auth: Auth | None = None,
|
||||
*,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
):
|
||||
self.username = username
|
||||
self._client = AsyncClient(
|
||||
base_url=base_url,
|
||||
@@ -77,9 +85,12 @@ class NextcloudClient:
|
||||
self.notes = NotesClient(self._client, username)
|
||||
self.webdav = WebDAVClient(self._client, username)
|
||||
self.tables = TablesClient(self._client, username)
|
||||
# CalendarClient takes raw credentials so caldav (which uses niquests as
|
||||
# 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, auth
|
||||
) # Uses AsyncDavClient internally
|
||||
base_url, username, password=password, token=token
|
||||
)
|
||||
self.contacts = ContactsClient(self._client, username)
|
||||
self.cookbook = CookbookClient(self._client, username)
|
||||
self.collectives = CollectivesClient(self._client, username)
|
||||
@@ -101,7 +112,12 @@ class NextcloudClient:
|
||||
username = os.environ["NEXTCLOUD_USERNAME"]
|
||||
password = os.environ["NEXTCLOUD_PASSWORD"]
|
||||
# Pass username to constructor
|
||||
return cls(base_url=host, username=username, auth=BasicAuth(username, password))
|
||||
return cls(
|
||||
base_url=host,
|
||||
username=username,
|
||||
auth=BasicAuth(username, password),
|
||||
password=password,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_token(cls, base_url: str, token: str, username: str):
|
||||
@@ -118,7 +134,12 @@ class NextcloudClient:
|
||||
from ..auth import BearerAuth # noqa: PLC0415
|
||||
|
||||
logger.info(f"Creating NC Client for user '{username}' using OAuth token")
|
||||
return cls(base_url=base_url, username=username, auth=BearerAuth(token))
|
||||
return cls(
|
||||
base_url=base_url,
|
||||
username=username,
|
||||
auth=BearerAuth(token),
|
||||
token=token,
|
||||
)
|
||||
|
||||
async def capabilities(self):
|
||||
response = await self._client.get(
|
||||
|
||||
@@ -10,7 +10,6 @@ import anyio
|
||||
from caldav.aio import AsyncCalendar, AsyncDAVClient, AsyncEvent
|
||||
from caldav.elements import cdav, dav
|
||||
from caldav.lib import error as caldav_error
|
||||
from httpx import Auth
|
||||
from icalendar import Alarm, Calendar, vDDDTypes, vRecur
|
||||
from icalendar import Event as ICalEvent
|
||||
from icalendar import Todo as ICalTodo
|
||||
@@ -36,22 +35,46 @@ async def _maybe_await(result: Any) -> Any:
|
||||
class CalendarClient:
|
||||
"""Client for Nextcloud CalDAV calendar and task operations."""
|
||||
|
||||
def __init__(self, base_url: str, username: str, auth: Auth | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
username: str,
|
||||
*,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
):
|
||||
"""Initialize CalendarClient with AsyncDAVClient.
|
||||
|
||||
Pass the raw credential plus an explicit ``auth_type`` so caldav can
|
||||
build whichever auth object its active HTTP backend needs. caldav v3
|
||||
prefers ``niquests`` over ``httpx`` and won't accept an ``httpx.Auth``
|
||||
when ``niquests`` is the active backend (issue #731), so we no longer
|
||||
accept a pre-built ``httpx.Auth`` here.
|
||||
|
||||
Args:
|
||||
base_url: Nextcloud base URL
|
||||
username: Nextcloud username
|
||||
auth: httpx.Auth object (BasicAuth or BearerAuth)
|
||||
password: App password / login password — selects ``auth_type="basic"``
|
||||
token: OAuth bearer token — selects ``auth_type="bearer"``
|
||||
|
||||
Pass exactly one of ``password`` or ``token``. Passing neither leaves
|
||||
the underlying client unauthenticated.
|
||||
"""
|
||||
self.username = username
|
||||
self.base_url = base_url
|
||||
|
||||
auth_kwargs: dict[str, Any] = {}
|
||||
if password is not None:
|
||||
auth_kwargs = {"password": password, "auth_type": "basic"}
|
||||
elif token is not None:
|
||||
auth_kwargs = {"password": token, "auth_type": "bearer"}
|
||||
|
||||
# AsyncDAVClient needs the full base URL for proper URL construction
|
||||
self._dav_client = AsyncDAVClient(
|
||||
url=f"{base_url}/remote.php/dav/",
|
||||
username=username,
|
||||
auth=auth,
|
||||
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,
|
||||
)
|
||||
self._calendar_home_url = f"{base_url}/remote.php/dav/calendars/{username}/"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user