feat(drive): dedup service move user_id to authz

This commit is contained in:
Edouard Vanbelle
2026-07-02 21:09:54 +02:00
parent a0e5c34950
commit 25dcab8e85
+82 -23
View File
@@ -608,17 +608,26 @@ impl DedupService {
} }
/// Of `hashes` (distinct), the subset `caller_id` may claim without /// Of `hashes` (distinct), the subset `caller_id` may claim without
/// uploading bytes: chunks referenced by manifests of the caller's /// uploading bytes: chunks referenced by manifests of files in drives
/// files (live or trashed), or directly referenced as (legacy) /// where the caller holds a **writable role** (owner / editor /
/// whole-file blobs. Backed by the GIN index on /// contributor), or directly referenced as (legacy) whole-file blobs
/// under the same predicate. Backed by the GIN index on
/// `chunk_manifests.chunk_hashes`. /// `chunk_manifests.chunk_hashes`.
/// ///
/// Post-D7 (`project_d7_policy_calls` LOCKED design): entitlement is
/// drive-membership + writable-role, not the legacy `user_id`
/// filter. Viewers/commenters are excluded — they can't legitimately
/// upload content into a drive, so they can't claim
/// "already-uploaded" via dedup. Group memberships (direct +
/// transitive) are expanded inline through
/// `storage.caller_group_ids($2)`.
///
/// Trashed files count as ownership: a trashed file's content is still /// Trashed files count as ownership: a trashed file's content is still
/// the caller's (restorable until trash-empty), so a re-upload of the /// under the caller's writable scope (restorable until trash-empty),
/// same content should hit the dedup fast path instead of forcing the /// so a re-upload of the same content should hit the dedup fast path
/// caller to re-send bytes they already have on the server. Must stay /// instead of forcing the caller to re-send bytes they already have
/// in lockstep with [`pin_claimable_chunks`], which actually bumps the /// on the server. Must stay in lockstep with [`pin_claimable_chunks`],
/// ref_count using the same entitlement set. /// which actually bumps the ref_count using the same entitlement set.
pub async fn claimable_chunks( pub async fn claimable_chunks(
&self, &self,
caller_id: uuid::Uuid, caller_id: uuid::Uuid,
@@ -633,13 +642,35 @@ impl DedupService {
SELECT 1 SELECT 1
FROM storage.files f FROM storage.files f
JOIN storage.chunk_manifests m ON m.file_hash = f.blob_hash JOIN storage.chunk_manifests m ON m.file_hash = f.blob_hash
WHERE f.user_id = $2 WHERE m.chunk_hashes @> ARRAY[c.h]
AND m.chunk_hashes @> ARRAY[c.h] AND EXISTS (
SELECT 1 FROM storage.role_grants g
WHERE g.resource_type = 'drive'
AND g.resource_id = f.drive_id
AND g.role IN ('owner', 'editor', 'contributor')
AND (g.expires_at IS NULL OR g.expires_at > NOW())
AND (
(g.subject_type = 'user' AND g.subject_id = $2)
OR (g.subject_type = 'group' AND g.subject_id IN
(SELECT storage.caller_group_ids($2)))
)
)
) )
OR EXISTS ( OR EXISTS (
SELECT 1 FROM storage.files f2 SELECT 1 FROM storage.files f2
WHERE f2.user_id = $2 WHERE f2.blob_hash = c.h
AND f2.blob_hash = c.h AND EXISTS (
SELECT 1 FROM storage.role_grants g
WHERE g.resource_type = 'drive'
AND g.resource_id = f2.drive_id
AND g.role IN ('owner', 'editor', 'contributor')
AND (g.expires_at IS NULL OR g.expires_at > NOW())
AND (
(g.subject_type = 'user' AND g.subject_id = $2)
OR (g.subject_type = 'group' AND g.subject_id IN
(SELECT storage.caller_group_ids($2)))
)
)
)", )",
) )
.bind(hashes) .bind(hashes)
@@ -651,17 +682,23 @@ impl DedupService {
} }
/// Pin one reference on each of `hashes` (distinct) that the caller is /// Pin one reference on each of `hashes` (distinct) that the caller is
/// entitled to claim — owned chunks (see [`claimable_chunks`]) or /// entitled to claim — writably-scoped chunks (see [`claimable_chunks`])
/// unreferenced orphans (`ref_count = 0`, the just-uploaded state). /// or unreferenced orphans (`ref_count = 0`, the just-uploaded state).
/// One statement: entitlement check and bump are atomic per row, so a /// One statement: entitlement check and bump are atomic per row, so a
/// concurrent last-reference delete can never be resurrected and a /// concurrent last-reference delete can never be resurrected and a
/// non-entitled hash is simply not returned. /// non-entitled hash is simply not returned.
/// ///
/// Entitlement includes files in trash: a trashed file is still owned /// Post-D7 (`project_d7_policy_calls` LOCKED): entitlement uses the
/// by the user, the content is still theirs to re-reference, and the /// same drive-membership + writable-role predicate as
/// race with trash-empty is handled the same way as `add_reference` — /// [`claimable_chunks`] — MUST STAY IN LOCKSTEP with that query.
/// if GC has already deleted the blob row, the UPDATE affects 0 rows /// Group memberships resolve through `storage.caller_group_ids($2)`.
/// and the hash is simply absent from the returned set. ///
/// Entitlement includes files in trash: a trashed file is still
/// within the caller's writable scope, the content is still theirs
/// to re-reference, and the race with trash-empty is handled the
/// same way as `add_reference` — if GC has already deleted the blob
/// row, the UPDATE affects 0 rows and the hash is simply absent from
/// the returned set.
/// ///
/// Returns the set actually pinned; the caller compares against its /// Returns the set actually pinned; the caller compares against its
/// input and reports the difference as `still_missing`. /// input and reports the difference as `still_missing`.
@@ -682,13 +719,35 @@ impl DedupService {
SELECT 1 SELECT 1
FROM storage.files f FROM storage.files f
JOIN storage.chunk_manifests m ON m.file_hash = f.blob_hash JOIN storage.chunk_manifests m ON m.file_hash = f.blob_hash
WHERE f.user_id = $2 WHERE m.chunk_hashes @> ARRAY[b.hash::text]
AND m.chunk_hashes @> ARRAY[b.hash::text] AND EXISTS (
SELECT 1 FROM storage.role_grants g
WHERE g.resource_type = 'drive'
AND g.resource_id = f.drive_id
AND g.role IN ('owner', 'editor', 'contributor')
AND (g.expires_at IS NULL OR g.expires_at > NOW())
AND (
(g.subject_type = 'user' AND g.subject_id = $2)
OR (g.subject_type = 'group' AND g.subject_id IN
(SELECT storage.caller_group_ids($2)))
)
)
) )
OR EXISTS ( OR EXISTS (
SELECT 1 FROM storage.files f2 SELECT 1 FROM storage.files f2
WHERE f2.user_id = $2 WHERE f2.blob_hash = b.hash
AND f2.blob_hash = b.hash AND EXISTS (
SELECT 1 FROM storage.role_grants g
WHERE g.resource_type = 'drive'
AND g.resource_id = f2.drive_id
AND g.role IN ('owner', 'editor', 'contributor')
AND (g.expires_at IS NULL OR g.expires_at > NOW())
AND (
(g.subject_type = 'user' AND g.subject_id = $2)
OR (g.subject_type = 'group' AND g.subject_id IN
(SELECT storage.caller_group_ids($2)))
)
)
) ) ) )
RETURNING b.hash", RETURNING b.hash",
) )