security(nextcloud): chroot-aware display paths + recent race fix

strip_chroot_prefix replaces the hardcoded "Personal/" strip
    in NC trashbin PROPFIND, OCS unified search, and REPORT
    (favorites + search). Handles composed chroots, drops
    cross-chroot items instead of surfacing malformed paths, and
    fixes the leading-slash mismatch (FolderDto path has '/', DB
    paths don't) that silently dropped every NC trashbin item
    post-D3. OCS keeps a first-segment fallback (results
    legitimately span drives, no single chroot).

    uploads_handler switches to nc_to_internal_path(chroot, …)
    for the two remaining hardcoded "Personal/" sites, closing
    the D1 TODO markers.

    RecentService::record_item_access is split from a new
    record_item_access_internal (no authz) used by
    RecentRecordingHook. Round 1's authz.require widened the
    tokio::spawn race past tests/api/recent.hurl step 7; the
    internal path skips the redundant Read gate — upstream
    _with_perms already enforced it.

    Tests: 8 unit tests pin strip_chroot_prefix (leading slash,
    composed chroots, sibling-leak rejection, partial-prefix,
    empty-chroot). drives_membership.hurl step 21b/22b cover
    Editor upload → 201 / Viewer upload → 404 fresh + overwrite
    with fixture cleanup at 30c. test_nc_move_copy_delete_trash
    K1 pins the actual original-location value.
This commit is contained in:
Edouard Vanbelle
2026-07-05 22:52:26 +02:00
parent 0342bae300
commit 1786fe4111
8 changed files with 446 additions and 54 deletions
+74 -3
View File
@@ -511,6 +511,26 @@ HTTP 200
jsonpath "$[*].id" contains {{team_drive_id}}
# ─────────────────────────────────────────────────────────────
# Step 21b — Upload gate by role (post-Drive AuthZ audit Round 2).
# Bob is Editor on team_drive; `POST /api/files/upload`
# targeting team_root_folder_id should succeed. This is
# the REST-side counterpart of the WebDAV/NC PUT chain
# hardened by `update_file_streaming_with_perms`. If
# this fails, the whole role-bundle → Permission::Create
# wiring is broken.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/files/upload
Authorization: Bearer {{bob_token}}
[MultipartFormData]
folder_id: {{team_root_folder_id}}
file: file,fixtures/hello.txt; text/plain
HTTP 201
[Captures]
bob_editor_upload_id: jsonpath "$.id"
# ─────────────────────────────────────────────────────────────
# Step 22 — Higher role wins: Bob now ALSO gets a Viewer direct
# grant (would lower his bundle). The collapsed caller_role
@@ -537,6 +557,50 @@ HTTP 200
jsonpath "$[*].id" contains {{team_drive_id}}
# ─────────────────────────────────────────────────────────────
# Step 22b — Viewer CANNOT upload into a shared drive.
# Post-Drive AuthZ audit Round 2: the create branch of
# `update_file_streaming_with_perms` requires
# `Permission::Create` on the parent folder — bundled
# with `owner`/`editor`/`contributor` role_grants only,
# NOT with `viewer`. `POST /api/files/upload` shares the
# same `save_file_with_blob` gate, so a Viewer probe
# must land 404 (anti-enum: same shape as no-such-folder)
# + `authz.denied` audit line. Also verify the batch /
# overwrite paths refuse — the whole chain from
# drive-membership to file write is exercised here.
# ─────────────────────────────────────────────────────────────
# 22b.i — Fresh file: 404.
POST {{base_url}}/api/files/upload
Authorization: Bearer {{bob_token}}
[MultipartFormData]
folder_id: {{team_root_folder_id}}
file: file,fixtures/hello.txt; text/plain
HTTP 404
# 22b.ii — Overwrite attempt on the Editor-era upload: still 404.
# `save_file_with_blob` catches the duplicate name at the
# `Create`-permission check before the upsert races (which
# would otherwise 409). The audit shape stays 404.
POST {{base_url}}/api/files/upload
Authorization: Bearer {{bob_token}}
[MultipartFormData]
folder_id: {{team_root_folder_id}}
file: file,fixtures/hello.txt; text/plain
HTTP 404
# 22b.iii — Alice's Editor-era file is untouched.
GET {{base_url}}/api/files/{{bob_editor_upload_id}}
Authorization: Bearer {{alice_token}}
HTTP 200
# =============================================================
# Per-role mutation matrix — what every role can / can't do
# =============================================================
@@ -845,15 +909,22 @@ HTTP 409
# 30c — Clear the lingering content (the Editor-created folder from
# Step 27). Delete via the regular folder endpoint so the row
# lands in trash, not the live tree; `is_empty` excludes
# trashed rows so a populated trash bin is allowed.
# Step 27 and the Editor-era file from Step 21b). Delete via
# the regular endpoints so rows land in trash, not the live
# tree; `is_empty` excludes trashed rows so a populated trash
# bin is allowed.
DELETE {{base_url}}/api/folders/{{editor_created_folder_id}}
Authorization: Bearer {{alice_token}}
HTTP 204
DELETE {{base_url}}/api/files/{{bob_editor_upload_id}}
Authorization: Bearer {{alice_token}}
HTTP 204
# 30d — Owner on an empty drive → 204.
DELETE {{base_url}}/api/drives/{{team_drive_id}}
Authorization: Bearer {{alice_token}}