Merge pull request #734 from cbcoutinho/fix/calendar-niquests-auth-731

fix(calendar): thread raw credentials to caldav AsyncDAVClient (fixes #731)
This commit is contained in:
Chris Coutinho
2026-04-29 23:04:50 +02:00
committed by GitHub
6 changed files with 156 additions and 9 deletions
@@ -65,6 +65,7 @@ async def _get_authenticated_client_for_userinfo(request: Request) -> NextcloudC
base_url=nextcloud_host,
username=username,
auth=BasicAuth(username, password),
password=password,
)
# OAuth mode - get token from session
+26 -5
View File
@@ -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(
+27 -4
View File
@@ -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}/"
+2
View File
@@ -148,6 +148,7 @@ def _get_client_from_basic_auth(ctx: Context) -> NextcloudClient:
base_url=settings.nextcloud_host,
username=username,
auth=BasicAuth(username, password),
password=password,
)
@@ -196,4 +197,5 @@ async def _get_client_from_login_flow(
base_url=nextcloud_host,
username=username,
auth=BasicAuth(username, app_data["app_password"]),
password=app_data["app_password"],
)
@@ -109,6 +109,7 @@ async def get_user_client_basic_auth(
base_url=nextcloud_host,
username=user_id,
auth=BasicAuth(user_id, app_password),
password=app_password,
)