Merge branch 'main' into claude/performance-optimization-round-6

Resolves the one conflict in file_blob_read_repository.rs's
suggest_files_by_name: main added the CALLER_CAN_READ_DRIVE authz scope
(caller_id param + drive-membership filter, AuthZ audit finding #1 — the
suggest query previously leaked names/paths across tenants), round 6
switched the same query's id/folder_id columns to binary UUID decode.
Kept both: main's authz structure (format! + CALLER_CAN_READ_DRIVE +
caller_id bind) with round 6's binary decode (fi.id / fi.folder_id, no
::text) so the query matches the FileRow = (Uuid, …) tuple. The
deliberately-text sites (min(fm.file_id::text), folder path lookup)
stay text. Verified: build + clippy -D warnings clean, 524 unit +
554 integration tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017aJu9ghvuT8WqC31ZEGTBA
This commit is contained in:
Claude
2026-07-18 10:07:42 +00:00
20 changed files with 523 additions and 293 deletions
@@ -1413,12 +1413,20 @@ impl FileReadPort for FileBlobReadRepository {
folder_id: Option<&str>,
query: &str,
limit: usize,
caller_id: Uuid,
) -> Result<Vec<File>, DomainError> {
// Scope by drive membership: `CALLER_CAN_READ_DRIVE` (`$1` =
// caller_id) restricts the result set to files whose owning drive
// the caller has any active `role_grants` on — direct or via a
// transitive group cascade. Pre-fix, the query only filtered on
// `NOT is_trashed AND name ILIKE $pattern`, exposing names + paths
// across every tenant on the instance (AuthZ audit finding #1,
// 2026-07-12).
let pattern = super::like_escape(query);
let limit_i64 = limit as i64;
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
sqlx::query_as(
sqlx::query_as(&format!(
r#"
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
@@ -1429,7 +1437,40 @@ impl FileReadPort for FileBlobReadRepository {
fi.created_by, fi.updated_by
FROM storage.files fi
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
WHERE fi.folder_id = $1::uuid
WHERE {CALLER_CAN_READ_DRIVE}
AND fi.folder_id = $2::uuid
AND NOT fi.is_trashed
AND fi.name ILIKE $3
ORDER BY CASE
WHEN fi.name ILIKE $4 THEN 0
WHEN fi.name ILIKE $4 || '%' THEN 1
ELSE 2
END,
fi.name
LIMIT $5
"#
))
.bind(caller_id)
.bind(fid)
.bind(&pattern)
.bind(query)
.bind(limit_i64)
.fetch_all(self.pool.as_ref())
.await
} else {
sqlx::query_as(&format!(
r#"
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
fi.blob_hash,
fi.created_by, fi.updated_by
FROM storage.files fi
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
WHERE {CALLER_CAN_READ_DRIVE}
AND fi.folder_id IS NULL
AND NOT fi.is_trashed
AND fi.name ILIKE $2
ORDER BY CASE
@@ -1439,38 +1480,9 @@ impl FileReadPort for FileBlobReadRepository {
END,
fi.name
LIMIT $4
"#,
)
.bind(fid)
.bind(&pattern)
.bind(query)
.bind(limit_i64)
.fetch_all(self.pool.as_ref())
.await
} else {
sqlx::query_as(
r#"
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
fi.blob_hash,
fi.created_by, fi.updated_by
FROM storage.files fi
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
WHERE fi.folder_id IS NULL
AND NOT fi.is_trashed
AND fi.name ILIKE $1
ORDER BY CASE
WHEN fi.name ILIKE $2 THEN 0
WHEN fi.name ILIKE $2 || '%' THEN 1
ELSE 2
END,
fi.name
LIMIT $3
"#,
)
"#
))
.bind(caller_id)
.bind(&pattern)
.bind(query)
.bind(limit_i64)
@@ -1232,31 +1232,39 @@ impl FolderRepository for FolderDbRepository {
parent_id: Option<&str>,
query: &str,
limit: usize,
caller_id: uuid::Uuid,
) -> Result<Vec<Folder>, DomainError> {
// Same drive-scope filter as `suggest_files_by_name` — closed as
// AuthZ audit finding #1 (2026-07-12). `CALLER_CAN_READ_DRIVE`
// aliases `storage.folders` as `fo`; the pre-fix query aliased it
// as an unqualified `storage.folders`, so this rewrite adds the
// `fo` alias in every branch.
let pattern = super::like_escape(query);
let limit_i64 = limit as i64;
let rows: Vec<FolderRow> = if let Some(pid) = parent_id {
sqlx::query_as(
sqlx::query_as(&format!(
r#"
SELECT id::text, name, path, parent_id::text, drive_id,
EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint,
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
created_by, updated_by
FROM storage.folders
WHERE parent_id = $1::uuid
AND NOT is_trashed
AND name ILIKE $2
SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, fo.drive_id,
EXTRACT(EPOCH FROM fo.created_at)::bigint,
EXTRACT(EPOCH FROM fo.updated_at)::bigint,
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint,
fo.created_by, fo.updated_by
FROM storage.folders fo
WHERE {CALLER_CAN_READ_DRIVE}
AND fo.parent_id = $2::uuid
AND NOT fo.is_trashed
AND fo.name ILIKE $3
ORDER BY CASE
WHEN name ILIKE $3 THEN 0
WHEN name ILIKE $3 || '%' THEN 1
WHEN fo.name ILIKE $4 THEN 0
WHEN fo.name ILIKE $4 || '%' THEN 1
ELSE 2
END,
name
LIMIT $4
"#,
)
fo.name
LIMIT $5
"#
))
.bind(caller_id)
.bind(pid)
.bind(&pattern)
.bind(query)
@@ -1264,26 +1272,28 @@ impl FolderRepository for FolderDbRepository {
.fetch_all(self.pool())
.await
} else {
sqlx::query_as(
sqlx::query_as(&format!(
r#"
SELECT id::text, name, path, parent_id::text, drive_id,
EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint,
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
created_by, updated_by
FROM storage.folders
WHERE parent_id IS NULL
AND NOT is_trashed
AND name ILIKE $1
SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, fo.drive_id,
EXTRACT(EPOCH FROM fo.created_at)::bigint,
EXTRACT(EPOCH FROM fo.updated_at)::bigint,
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint,
fo.created_by, fo.updated_by
FROM storage.folders fo
WHERE {CALLER_CAN_READ_DRIVE}
AND fo.parent_id IS NULL
AND NOT fo.is_trashed
AND fo.name ILIKE $2
ORDER BY CASE
WHEN name ILIKE $2 THEN 0
WHEN name ILIKE $2 || '%' THEN 1
WHEN fo.name ILIKE $3 THEN 0
WHEN fo.name ILIKE $3 || '%' THEN 1
ELSE 2
END,
name
LIMIT $3
"#,
)
fo.name
LIMIT $4
"#
))
.bind(caller_id)
.bind(&pattern)
.bind(query)
.bind(limit_i64)