security(search): ensure that search suggenstion returns answer the user has access to

This commit is contained in:
Edouard Vanbelle
2026-07-16 21:07:18 +02:00
parent 7d95a19907
commit c1924c825b
9 changed files with 171 additions and 83 deletions
+30 -7
View File
@@ -110,7 +110,7 @@ Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.files" count >= 1
jsonpath "$.files" count >= 1
body contains "{{needle_file_id}}"
@@ -124,7 +124,7 @@ Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.files" count == 0
jsonpath "$.files" count == 0
jsonpath "$.folders" count == 0
@@ -152,6 +152,29 @@ body not contains "unique-search-needle"
body not contains "{{needle_file_id}}"
# ─────────────────────────────────────────────────────────────
# 5b — REGRESSION: `/api/search/suggest` MUST also refuse to
# surface admin's file to bob. Pre-fix (AuthZ audit #1,
# 2026-07-12) the suggest endpoint had NO `AuthUser`
# extractor and its underlying `suggest_files_by_name` /
# `suggest_folders_by_name` filtered only on
# `NOT is_trashed AND name ILIKE $1` — any authenticated
# user (including externals) could autocomplete names and
# full `path` values across every tenant on the instance.
# Fix: added `caller_id` to both repo queries via the
# shared `CALLER_CAN_READ_DRIVE` predicate (`role_grants`
# + `caller_group_ids`). This assertion is the anti-
# regression pin.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/search/suggest?query=unique-search-needle
Authorization: Bearer {{bob_token}}
HTTP 200
[Asserts]
body not contains "unique-search-needle"
body not contains "{{needle_file_id}}"
# ─────────────────────────────────────────────────────────────
# 6 — CONTENT-search cross-drive isolation (docs/plan/drive.md §11).
# The cross-user check above (step 5) verifies the NAME-search
@@ -209,7 +232,7 @@ HTTP 200
# Bob has no access to admin's drive → Tantivy's Must-clause
# filters every doc that doesn't carry one of Bob's drive_ids,
# so the file vanishes entirely.
jsonpath "$.files" count == 0
jsonpath "$.files" count == 0
jsonpath "$.folders" count == 0
body not contains "{{canary_file_id}}"
body not contains "ContentIndexCanaryXyzzy2026Drive"
@@ -221,11 +244,11 @@ body not contains "ContentIndexCanaryXyzzy2026Drive"
# other field names below MUST stay absent: a future field
# called `hidden_count`/`filtered`/etc. that reveals matches
# Bob can't see would be the regression.
jsonpath "$.total_count" == 0
jsonpath "$.has_more" == false
jsonpath "$.total_count" == 0
jsonpath "$.has_more" == false
jsonpath "$.hidden_count" not exists
jsonpath "$.filtered" not exists
jsonpath "$.total" not exists
jsonpath "$.filtered" not exists
jsonpath "$.total" not exists
# ─────────────────────────────────────────────────────────────