Files
Oxicloud/tests/api/webdav_permissions.hurl
T
Edouard Vanbelle 7e34045ff8 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
2026-07-06 22:14:50 +02:00

301 lines
15 KiB
Plaintext

# =============================================================
# OxiCloud — WebDAV per-role permissions + cross-drive MOVE policy
# =============================================================
# End-to-end coverage for the two WebDAV authz axes exposed by the
# `@drive` URL scheme:
#
# 1. Per-role gates through the drive-scope resolver: a Viewer on a
# shared drive can PROPFIND/GET but cannot MKCOL/PUT/MOVE. An
# Editor can. AuthZ denials return `NotFound` (anti-enum), so
# a probing caller can't tell a genuinely-missing folder from
# one they simply lack Create on.
#
# 2. Drive policy `forbid_cross_drive_move` gates MOVE at the
# SOURCE drive (see `DrivePolicies::refuse_cross_drive_move`
# in `src/domain/entities/drive.rs`) — even a fully-authorised
# Editor can't move content OUT of a drive whose owner has
# forbidden cross-drive movement. Rejection is 405
# (`ErrorKind::UnsupportedOperation` → `METHOD_NOT_ALLOWED`).
#
# Assumes the default `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX="@drive"` config —
# runs alongside the other tests in `tests/api/run.sh`. Uses the
# `@drive/<uuid>` selector so the paths don't collide with any
# drive-name-collision oddities.
# =============================================================
# ─────────────────────────────────────────────────────────────
# Step 1 — Login as admin (bootstrapped by `setup.hurl`).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
admin_token: jsonpath "$.access_token"
admin_user_id: jsonpath "$.user.id"
# ─────────────────────────────────────────────────────────────
# Step 2 — Create a fresh user "webdav_bob" via the admin
# endpoint, log him in.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/admin/users
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"username": "webdav_bob",
"password": "WebdavBobPassword1!",
"email": "webdav_bob@example.com",
"role": "user"
}
HTTP 201
[Captures]
bob_user_id: jsonpath "$.id"
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "webdav_bob", "password": "WebdavBobPassword1!" }
HTTP 200
[Captures]
bob_token: jsonpath "$.access_token"
# Capture Bob's default personal drive id — used by the cross-drive
# MOVE scenario. Bob is not a member of any shared drive yet, so his
# `/api/drives` listing has exactly one entry (his own default).
GET {{base_url}}/api/drives
Authorization: Bearer {{bob_token}}
HTTP 200
[Captures]
bob_personal_drive_id: jsonpath "$[0].id"
# ─────────────────────────────────────────────────────────────
# Step 3 — Admin creates a shared drive owned by admin.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/drives
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"kind": "shared",
"name": "webdav-perm-shared",
"owner": { "type": "user", "id": "{{admin_user_id}}" }
}
HTTP 201
[Captures]
shared_drive_id: jsonpath "$.id"
# ─────────────────────────────────────────────────────────────
# Step 4 — Grant Bob VIEWER on the shared drive via /api/grants.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/grants
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"subject": { "type": "user", "id": "{{bob_user_id}}" },
"resource": { "type": "drive", "id": "{{shared_drive_id}}" },
"role": "viewer"
}
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 5 — Bob (VIEWER) CAN PROPFIND the shared drive root.
# Depth 0 to keep the assertion minimal; a 207 with the
# drive's own href suffices as "Bob has Read".
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/webdav/@drive/{{shared_drive_id}}/
Authorization: Bearer {{bob_token}}
Depth: 0
HTTP 207
# ─────────────────────────────────────────────────────────────
# Step 6 — Bob (VIEWER) CANNOT MKCOL on the shared drive.
# `authz.require(Create, Folder)` denial returns
# `DomainError::not_found` (anti-enum), which maps to 404.
# ─────────────────────────────────────────────────────────────
MKCOL {{base_url}}/webdav/@drive/{{shared_drive_id}}/viewer-blocked-folder
Authorization: Bearer {{bob_token}}
HTTP 404
# ─────────────────────────────────────────────────────────────
# Step 7 — Bob (VIEWER) CANNOT PUT a file.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/webdav/@drive/{{shared_drive_id}}/viewer-blocked-file.txt
Authorization: Bearer {{bob_token}}
Content-Type: text/plain
```
viewer should not upload
```
HTTP 404
# ─────────────────────────────────────────────────────────────
# Step 8 — Admin creates a probe folder in the shared drive so
# the Editor-can-rename step below has a real target.
# ─────────────────────────────────────────────────────────────
MKCOL {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder
Authorization: Bearer {{admin_token}}
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 9 — Bob (VIEWER) CANNOT MOVE (rename) the probe folder.
# MOVE requires Update on the source, which Viewer
# doesn't have. Same anti-enum 404 shape.
# ─────────────────────────────────────────────────────────────
MOVE {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder
Authorization: Bearer {{bob_token}}
Destination: {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder-renamed
HTTP 404
# ─────────────────────────────────────────────────────────────
# Step 10 — Promote Bob from VIEWER to EDITOR.
# `PATCH /api/drives/{id}/members/{subject-type}/{id}`
# mutates the role in-place.
# ─────────────────────────────────────────────────────────────
PATCH {{base_url}}/api/drives/{{shared_drive_id}}/members/user/{{bob_user_id}}
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{ "role": "editor" }
HTTP 200
# ─────────────────────────────────────────────────────────────
# Step 11 — Bob (EDITOR) CAN MKCOL a new folder.
# ─────────────────────────────────────────────────────────────
MKCOL {{base_url}}/webdav/@drive/{{shared_drive_id}}/editor-created-folder
Authorization: Bearer {{bob_token}}
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 12 — Bob (EDITOR) CAN PUT a file.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/webdav/@drive/{{shared_drive_id}}/editor-created-folder/hello.txt
Authorization: Bearer {{bob_token}}
Content-Type: text/plain
```
editor uploaded content
```
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 13 — Bob (EDITOR) CAN MOVE (rename) the probe folder.
# ─────────────────────────────────────────────────────────────
MOVE {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder
Authorization: Bearer {{bob_token}}
Destination: {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder-renamed
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 14 — Bob puts a file in his OWN personal drive as the
# source for the cross-drive MOVE test below.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/webdav/xdrive-probe.txt
Authorization: Bearer {{bob_token}}
Content-Type: text/plain
```
cross-drive probe payload
```
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 15 — Admin flips `forbid_cross_drive_move` ON for Bob's
# PERSONAL drive. The policy sits on the SOURCE drive
# per `DrivePolicies::refuse_cross_drive_move`; only
# OxiCloud-admin can PATCH policies.
# ─────────────────────────────────────────────────────────────
PATCH {{base_url}}/api/drives/{{bob_personal_drive_id}}/policies
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{ "forbid_cross_drive_move": true }
HTTP 200
[Asserts]
jsonpath "$.forbid_cross_drive_move" == true
# ─────────────────────────────────────────────────────────────
# Step 16 — Bob tries to MOVE `xdrive-probe.txt` from his
# PERSONAL drive to the SHARED drive. Blocked at the
# service layer by the policy — `OperationNotSupported`
# maps to 405 Method Not Allowed.
# ─────────────────────────────────────────────────────────────
MOVE {{base_url}}/webdav/xdrive-probe.txt
Authorization: Bearer {{bob_token}}
Destination: {{base_url}}/webdav/@drive/{{shared_drive_id}}/xdrive-probe.txt
HTTP 405
# ─────────────────────────────────────────────────────────────
# Step 17 — Admin flips the policy OFF.
# ─────────────────────────────────────────────────────────────
PATCH {{base_url}}/api/drives/{{bob_personal_drive_id}}/policies
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{ "forbid_cross_drive_move": false }
HTTP 200
[Asserts]
jsonpath "$.forbid_cross_drive_move" == false
# ─────────────────────────────────────────────────────────────
# Step 18 — Bob retries the same MOVE. Now the policy is off,
# Bob has Update on source (his own personal drive) +
# Create on dest parent (Editor on shared drive), so
# the move succeeds. 201 on rename/move to a new URL,
# per `handle_move`'s existing convention.
# ─────────────────────────────────────────────────────────────
MOVE {{base_url}}/webdav/xdrive-probe.txt
Authorization: Bearer {{bob_token}}
Destination: {{base_url}}/webdav/@drive/{{shared_drive_id}}/xdrive-probe.txt
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 19 — Verify the destination now exists and the source
# is gone. Both PROPFINDs use Bob's token to also
# re-confirm the AuthZ gates on the destination side.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/webdav/@drive/{{shared_drive_id}}/xdrive-probe.txt
Authorization: Bearer {{bob_token}}
Depth: 0
HTTP 207
PROPFIND {{base_url}}/webdav/xdrive-probe.txt
Authorization: Bearer {{bob_token}}
Depth: 0
HTTP 404