feat: remove global Nextcloud config, enforce member-specific storage providers
- Remove global Nextcloud settings from config - Make NextcloudClient require explicit credentials - Update for_member() to return None when no credentials - Modify services to accept optional storage client - Update routers to pass member storage to services - Add 403 responses when no storage provider configured - Update internal endpoints to use member storage credentials This change enforces that each member must configure their own Nextcloud storage provider. If no provider is configured, file operations will return 403 FORBIDDEN instead of falling back to global placeholders.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Any
|
||||
|
||||
@@ -10,31 +11,34 @@ import httpx
|
||||
from rehearsalhub.config import get_settings
|
||||
from rehearsalhub.storage.protocol import FileMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DAV_NS = "{DAV:}"
|
||||
|
||||
|
||||
class NextcloudClient:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str | None = None,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
s = get_settings()
|
||||
self._base = (base_url or s.nextcloud_url).rstrip("/")
|
||||
self._auth = (username or s.nextcloud_user, password or s.nextcloud_pass)
|
||||
if not base_url or not username:
|
||||
raise ValueError("Nextcloud credentials must be provided explicitly")
|
||||
self._base = base_url.rstrip("/")
|
||||
self._auth = (username, password)
|
||||
self._dav_root = f"{self._base}/remote.php/dav/files/{self._auth[0]}"
|
||||
|
||||
@classmethod
|
||||
def for_member(cls, member: object) -> "NextcloudClient":
|
||||
"""Return a client using member's personal NC credentials if configured,
|
||||
falling back to the global env-var credentials."""
|
||||
def for_member(cls, member: object) -> "NextcloudClient | None":
|
||||
"""Return a client using member's personal NC credentials if configured.
|
||||
Returns None if member has no Nextcloud configuration."""
|
||||
nc_url = getattr(member, "nc_url", None)
|
||||
nc_username = getattr(member, "nc_username", None)
|
||||
nc_password = getattr(member, "nc_password", None)
|
||||
if nc_url and nc_username and nc_password:
|
||||
return cls(base_url=nc_url, username=nc_username, password=nc_password)
|
||||
return cls()
|
||||
return None
|
||||
|
||||
def _client(self) -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(auth=self._auth, timeout=30.0)
|
||||
@@ -83,10 +87,23 @@ class NextcloudClient:
|
||||
return _parse_propfind_multi(resp.text)
|
||||
|
||||
async def download(self, path: str) -> bytes:
|
||||
async with self._client() as c:
|
||||
resp = await c.get(self._dav_url(path))
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
logger.debug("Downloading file from Nextcloud: %s", path)
|
||||
try:
|
||||
async with self._client() as c:
|
||||
resp = await c.get(self._dav_url(path))
|
||||
resp.raise_for_status()
|
||||
logger.debug("Successfully downloaded file: %s", path)
|
||||
return resp.content
|
||||
except httpx.ConnectError as e:
|
||||
logger.error("Failed to connect to Nextcloud at %s: %s", self._base, str(e))
|
||||
raise
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error("Nextcloud request failed for %s: %s (status: %d)",
|
||||
path, str(e), e.response.status_code)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error downloading from Nextcloud: %s", str(e))
|
||||
raise
|
||||
|
||||
async def get_direct_url(self, path: str) -> str:
|
||||
return self._dav_url(path)
|
||||
|
||||
Reference in New Issue
Block a user