feat(antienum): 403 when sub can read, 404 otherwise

this is a UX improvement, always return a 404 not found when subject do not have any access on the resource
    but returns an explicit 403 forbidden is subject try a forbidden action on a resourse it can read

    regarding performance, the role is already in cache for the second call with read perm
This commit is contained in:
Edouard Vanbelle
2026-07-16 21:34:31 +02:00
parent 5b996bb218
commit 7aea383588
9 changed files with 252 additions and 153 deletions
+26 -21
View File
@@ -6,9 +6,10 @@
#
# 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.
# Editor can. AuthZ denials use graduated shape: a caller with
# Read on the target (Viewer here) gets 403 Forbidden — no point
# hiding existence from someone already reading it. A caller with
# no Read at all gets 404 (anti-enum), matching "no such folder".
#
# 2. Drive policy `forbid_cross_drive_move` gates MOVE at the
# SOURCE drive (see `DrivePolicies::refuse_cross_drive_move`
@@ -123,17 +124,22 @@ 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.
# `authz.require(Create, Folder)` denies. Bob has Read
# on the drive (viewer role) → engine's graduated denial
# returns `DomainError::access_denied` → 403 Forbidden.
# Anti-enum still holds for callers with no Read at all
# (would surface as 404); this is the "you can see it,
# but can't touch it" branch.
# ─────────────────────────────────────────────────────────────
MKCOL {{base_url}}/webdav/@drive/{{shared_drive_id}}/viewer-blocked-folder
Authorization: Bearer {{bob_token}}
HTTP 404
HTTP 403
# ─────────────────────────────────────────────────────────────
# Step 7 — Bob (VIEWER) CANNOT PUT a file.
# Step 7 — Bob (VIEWER) CANNOT PUT a file. Same 403 shape
# (Bob has Read on the drive).
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/webdav/@drive/{{shared_drive_id}}/viewer-blocked-file.txt
Authorization: Bearer {{bob_token}}
@@ -142,7 +148,7 @@ Content-Type: text/plain
viewer should not upload
```
HTTP 404
HTTP 403
# ─────────────────────────────────────────────────────────────
@@ -158,48 +164,47 @@ 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.
# doesn't have. Bob can Read the folder (viewer) → 403.
# ─────────────────────────────────────────────────────────────
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
HTTP 403
# ─────────────────────────────────────────────────────────────
# Step 9b — Bob (VIEWER) CANNOT COPY the probe folder.
# COPY requires Create on the destination parent, which
# Viewer doesn't have. Anti-enum 404 shape.
# Viewer doesn't have. Bob has Read on both source and
# destination parent → 403 (graduated denial).
#
# This is the regression pin for AuthZ audit #2
# (2026-07-12): the COPY handler used to `map_err(|e|
# AppError::internal_error(format!("Failed to copy folder
# tree: {}", e)))?` on `copy_folder_tree_with_perms`,
# which collapsed the `NotFound` that `authz.require`
# returns on denial into HTTP 500 — an "exists-but-denied"
# oracle. Fix routes through `AppError::from` so the same
# denial surfaces as 404, indistinguishable from a source
# path that simply doesn't exist.
# collapsing the `DomainError` engine returned on denial
# into HTTP 500 — an "exists-but-denied" oracle. Fix
# routes through `AppError::from` so the same denial
# surfaces as the correct 403 / 404 per graduated-denial
# policy.
# ─────────────────────────────────────────────────────────────
COPY {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder
Authorization: Bearer {{bob_token}}
Destination: {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder-copy
HTTP 404
HTTP 403
# ─────────────────────────────────────────────────────────────
# Step 9c — Bob (VIEWER) CANNOT DELETE the probe folder.
# DELETE requires Delete on the target, which Viewer
# doesn't have. Anti-enum 404 shape — same regression
# pin as 9b (`map_err → internal_error` collapsed
# the `NotFound` from authz.require into a 500 oracle).
# doesn't have. Bob has Read → 403.
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder
Authorization: Bearer {{bob_token}}
HTTP 404
HTTP 403
# ─────────────────────────────────────────────────────────────