7e34045ff8
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
301 lines
15 KiB
Plaintext
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
|