feat(drive): fix webdav back-compat

add env variable `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX`
    which is by default:
    `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX="@drive"`

    so `/webdav/` -> points to user's personal drive (**backward compatibilit**y)
    `/web/dav/@drive/{uuid|drive name}/` points to the respective drive

    if admins want directly `/webdav/` pointing to list of drives they need to:
    `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX=""`

    + ensure lock is per user (RFC 4918 §9.11)

    fix: #554
This commit is contained in:
Edouard Vanbelle
2026-07-06 20:45:21 +02:00
parent 3d74bed326
commit 7e34045ff8
16 changed files with 1576 additions and 232 deletions
+1
View File
@@ -70,6 +70,7 @@ Most runtime variables use the `OXICLOUD_` prefix. A few build-time or allocator
| `OXICLOUD_ENABLE_MUSIC` | `true` | Music playlists and audio metadata |
| `OXICLOUD_EXPOSE_SYSTEM_USERS` | `true` | Expose other OxiCloud users as a read-only address book at `GET /api/address-books` |
| `OXICLOUD_ENABLE_ADMIN_INTERNAL_ENDPOINTS` | `false` | Expose `POST /api/admin/internal/trigger-sweep` and `POST /api/admin/internal/trigger-gc` — test-only synchronous triggers for the storage-usage reconciliation sweep and blob garbage collector. Used by the API test suite to assert post-delete quota convergence without waiting out the periodic ticker. Leave **off** in production: the routes return 404 even to an admin token when disabled. |
| `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX` | `@drive` | Native WebDAV URL segment that renders the caller's drive list. Sanitized by trimming leading/trailing `/`. Three shapes: (1) default `@drive` — `/webdav/…` addresses the caller's default personal drive (back-compat), `/webdav/@drive/` returns the drive listing, `/webdav/@drive/<uuid\|name>/…` targets a specific drive. (2) empty string `""` — `/webdav/` IS the drive listing, `/webdav/<uuid\|name>/…` targets a specific drive, no default-drive shortcut. (3) any other string (e.g. `drives`) — same shape as `@drive` with that segment substituted. Only drives the caller has Read on via `role_grants` resolve. |
## Storage Backend
+56 -32
View File
@@ -761,15 +761,24 @@ accommodates them without schema migration)
#### Native WebDAV (`/webdav/...`)
| URL | Resolves to |
|---|---|
| `/webdav/<path>` | Caller's default personal drive root + `<path>` (back-compat with today's behaviour) |
| `/webdav/@drive/<drive-uuid>/<path>` | Specific drive root + `<path>` |
**SHIPPED 2026-07-06.** Config-driven via env
`OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX` (`FeaturesConfig::webdav_drive_listing_prefix`;
default `"@drive"`, sanitized by trimming leading/trailing `/`).
Three deployment shapes:
Today's `/webdav/<path>` handler implicitly looks up the caller's
home folder and prepends it. Post-drives, the same handler looks up
the caller's personal drive and resolves paths inside it. **Zero
breakage** for existing native WebDAV clients.
| `WEBDAV_DRIVE_LISTING_PREFIX` | URL | Resolves to |
|---|---|---|
| `@drive` (default) | `/webdav/…` | caller's default personal drive (back-compat) |
| `@drive` | `/webdav/@drive/` | drive listing |
| `@drive` | `/webdav/@drive/<sel>/…` | specific drive |
| `""` (empty) | `/webdav/` | drive listing |
| `""` | `/webdav/<sel>/…` | specific drive |
| any other | same shape as `@drive`, segment substituted | |
`<sel>` is a drive UUID **or** the drive's display name (matched
against `storage.folders.name` of the drive root). Only drives the
caller has Read on via `role_grants` resolve; unknown selector and
permission denial both return 404 (anti-enumeration).
**Why the `@drive` sigil and NOT `/webdav/drives/<uuid>/...`**
(earlier draft) or top-level `/drives/<uuid>/...` (also
@@ -777,32 +786,47 @@ considered): `@` is the established structural-routing sigil
(GitHub `@user/repo`, npm `@scope/pkg`, LDAP `@domain`) — it
reads as "this is not user content, this is a routing token."
Realistic collision risk drops to near-zero: nobody creates a
top-level folder named exactly `@drive` by accident, and the
defensive layer collapses to a single one-liner in MKCOL / PUT /
REST create paths that refuses that literal name at any drive
root. Compared to top-level `/drives/<uuid>/...`, the `@drive`
shape keeps **one URL root for everything WebDAV** — single
`<Location>` block in reverse-proxy configs, single mental model
for sysadmins, single dispatcher in `webdav_routes()`.
top-level folder named exactly `@drive` by accident. Keeps **one
URL root for everything WebDAV** — single `<Location>` block in
reverse-proxy configs, single mental model for sysadmins, single
dispatcher in `webdav_routes()`. Making the segment
config-tunable per deployment lets operators pick a different
sigil (`drives`) or drop it entirely (`""` = drive-listing at
root) without a code change.
**Implementation notes:**
- Route parser accepts both `/webdav/@drive/<uuid>/...` and the
URL-encoded form `/webdav/%40drive/<uuid>/...` — WebDAV clients
percent-encode `@` inconsistently.
- One-liner guard in upload paths refuses creation of a folder
literally named `@drive` at any drive root (case-sensitive).
- `webdav_href()` (today at `webdav_handler.rs:94`) becomes
drive-context-aware: responses for a request under
`/webdav/@drive/<uuid>/...` must reference back to
`/webdav/@drive/<uuid>/...`, otherwise the client follows the
`<D:href>` and lands on the back-compat surface (wrong drive).
**Implementation:** `resolve_webdav_scope` in
`src/interfaces/api/handlers/webdav_handler.rs`. Selector accepts
UUIDs and display names; UUID form is tried first. Legacy
tolerance in the default-drive branch: bookmarks that already
carried the drive-root name as their first segment
(`/webdav/Personal/foo` under a Personal-default user) are
passed through instead of double-prepended.
The `drives` path segment is **reserved**: a folder literally named
`drives` cannot exist at the top level of any drive. Migration
pre-check refuses to start if existing data violates this — operator
must rename before upgrading. (Conservative estimate: zero existing
folders are named exactly `drives`. The migration script reports any
collisions for manual fix-up.)
**Hurl coverage:**
- `tests/api/webdav_drive_root.hurl` — default `@drive` config
- `tests/webdav-drive-root/drive_root_empty_config.hurl` — empty
config (separately-configured server; runs under
`tests/webdav-drive-root/run.sh`, wired into `just api-test`
and CI's `api-test` job)
**Href construction — verified drive-aware:** `webdav_href()`
prints `/webdav/<path>`, but the `<path>` input is `client_path`
extracted from `req.uri()` (the URL segment after `/webdav/`), not
the scope-resolved db_path. So a request to
`/webdav/@drive/<sel>/folder/` renders children as
`/webdav/@drive/<sel>/folder/<child>/` — the `@drive/<sel>/`
prefix is preserved on every hop. `client_path` is threaded into
`base_href` at `handle_propfind` and passed through
`build_streaming_propfind_response` unchanged.
**Deferred (not blocking):**
- One-liner guard refusing folder creation named literally
`@drive` at drive root (defensive against future collisions —
today an unknown `@drive` folder at drive root is unreachable
via WebDAV under the default config, so it's low priority).
- Cross-drive MOVE / COPY currently 403 — same-drive only.
Cross-drive copy has REST-side support; WebDAV MOVE/COPY
could route through it once permission mapping is designed.
#### NextCloud-compat WebDAV (`/remote.php/dav/...`)