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
+28 -28
View File
@@ -564,34 +564,35 @@ jsonpath "$[*].id" contains {{team_drive_id}}
# `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.
# same `save_file_with_blob` gate. Bob has Read on the
# drive (viewer role cascades) → graduated denial returns
# 403 (see [[project_authz_require_graduated_denial]]).
# Also verify the batch / overwrite paths refuse — the
# whole chain from drive-membership to file write is
# exercised here.
# ─────────────────────────────────────────────────────────────
# 22b.i — Fresh file: 404.
# 22b.i — Fresh file: 403.
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
HTTP 403
# 22b.ii — Overwrite attempt on the Editor-era upload: still 404.
# 22b.ii — Overwrite attempt on the Editor-era upload: still 403.
# `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.
# would otherwise 409).
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
HTTP 403
# 22b.iii — Alice's Editor-era file is untouched.
@@ -698,8 +699,8 @@ jsonpath "$.role" == "viewer"
# ─────────────────────────────────────────────────────────────
# Step 25 — Viewer CANNOT edit drive members.
# Bob is Viewer. Every member-mutation verb → 404
# (anti-enum: same shape as if the drive didn't exist).
# Bob is Viewer (has Read on the drive) → graduated denial
# returns 403 on every member-mutation verb.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/drives/{{team_drive_id}}/members
Authorization: Bearer {{bob_token}}
@@ -709,7 +710,7 @@ Content-Type: application/json
"role": "editor"
}
HTTP 404
HTTP 403
PATCH {{base_url}}/api/drives/{{team_drive_id}}/members/user/{{carol_user_id}}
@@ -717,13 +718,13 @@ Authorization: Bearer {{bob_token}}
Content-Type: application/json
{ "role": "viewer" }
HTTP 404
HTTP 403
DELETE {{base_url}}/api/drives/{{team_drive_id}}/members/user/{{carol_user_id}}
Authorization: Bearer {{bob_token}}
HTTP 404
HTTP 403
# ─────────────────────────────────────────────────────────────
@@ -739,7 +740,7 @@ Content-Type: application/json
HTTP 200
# 26a — Editor POST /api/drives/{id}/members → 404.
# 26a — Editor POST /api/drives/{id}/members → 403 (Editor has Read).
POST {{base_url}}/api/drives/{{team_drive_id}}/members
Authorization: Bearer {{bob_token}}
Content-Type: application/json
@@ -748,39 +749,39 @@ Content-Type: application/json
"role": "viewer"
}
HTTP 404
HTTP 403
# 26b — Editor PATCH a member → 404.
# 26b — Editor PATCH a member → 403.
PATCH {{base_url}}/api/drives/{{team_drive_id}}/members/user/{{carol_user_id}}
Authorization: Bearer {{bob_token}}
Content-Type: application/json
{ "role": "viewer" }
HTTP 404
HTTP 403
# 26c — Editor DELETE a member → 404.
# 26c — Editor DELETE a member → 403.
DELETE {{base_url}}/api/drives/{{team_drive_id}}/members/user/{{carol_user_id}}
Authorization: Bearer {{bob_token}}
HTTP 404
HTTP 403
# 26d — Editor renames the drive (root folder) → 404.
# 26d — Editor renames the drive (root folder) → 403.
# Folder rename normally requires `Permission::Update` (which
# Editor has on every folder in the drive via the engine's drive
# precheck). The folder service promotes the requirement to
# `Permission::Manage` when the target folder has `parent_id IS
# NULL` — i.e. it's a drive root — so the drive-rename surface is
# Owner-only per drive.md §6, without changing the public folder
# endpoint shape. Anti-enum: refusal returns 404 (not 403).
# endpoint shape. Editor has Read → graduated denial → 403.
PUT {{base_url}}/api/folders/{{team_root_folder_id}}/rename
Authorization: Bearer {{bob_token}}
Content-Type: application/json
{ "name": "team-drive-editor-renamed" }
HTTP 404
HTTP 403
# ─────────────────────────────────────────────────────────────
@@ -883,7 +884,7 @@ HTTP 404
# ─────────────────────────────────────────────────────────────
# Step 30 — Drive delete (D3b).
# - Non-Owner → 404 (Bob is Viewer post-Step 28).
# - Non-Owner → 403 (Bob is Viewer post-Step 28, has Read).
# - Owner on non-empty drive → 409 (the editor-created-folder
# from Step 27 is still live).
# - Owner after the folder is trashed → 204.
@@ -892,12 +893,11 @@ HTTP 404
# we exercise its 405 below.
# ─────────────────────────────────────────────────────────────
# 30a — Viewer (Bob) cannot delete the drive → 404, anti-enum same as
# the member-mutation refusals.
# 30a — Viewer (Bob) cannot delete the drive → 403 (has Read).
DELETE {{base_url}}/api/drives/{{team_drive_id}}
Authorization: Bearer {{bob_token}}
HTTP 404
HTTP 403
# 30b — Owner (Alice) on a non-empty drive → 409 with the canonical