diff --git a/src/application/services/share_browse_service.rs b/src/application/services/share_browse_service.rs index a68d6685..a26a88b3 100644 --- a/src/application/services/share_browse_service.rs +++ b/src/application/services/share_browse_service.rs @@ -116,19 +116,35 @@ impl ShareBrowseService { self.list_inner(folder_id, resolved.owner_id).await } + /// AuthZ gate for `/api/s/{token}/file/{file_id}`: the requested file + /// must either BE the shared item (single-file share — the public landing + /// page's inline media preview streams through here) or live inside the + /// shared folder's subtree (folder share). Anything else is NotFound — + /// the same shape as "file doesn't exist", so the endpoint can't be used + /// to enumerate file ids. pub async fn assert_file_in_share( &self, token: &str, file_id: &str, unlock_jwt: Option<&str>, ) -> Result<(), DomainError> { - let resolved = self.resolve_folder_share(token, unlock_jwt).await?; + let share = self + .share_service + .get_shared_link_with_unlock(token, unlock_jwt) + .await?; - if !self - .folder_repo - .is_file_in_subtree(file_id, &resolved.root_folder_id) - .await? - { + let in_scope = match share.item_type.as_str() { + // Single-file share: only the shared item itself may be streamed. + "file" => file_id == share.item_id, + // Folder share: the file must live in the shared subtree. + "folder" => { + self.folder_repo + .is_file_in_subtree(file_id, &share.item_id) + .await? + } + _ => false, + }; + if !in_scope { return Err(DomainError::not_found("File", file_id)); } Ok(()) diff --git a/src/interfaces/api/handlers/share_handler.rs b/src/interfaces/api/handlers/share_handler.rs index bc20fe12..0cc2dd58 100644 --- a/src/interfaces/api/handlers/share_handler.rs +++ b/src/interfaces/api/handlers/share_handler.rs @@ -639,7 +639,7 @@ pub async fn list_share_contents_subfolder( path = "/api/s/{token}/file/{file_id}", params( ("token" = String, Path, description = "Share token"), - ("file_id" = String, Path, description = "File ID (must be inside the share)") + ("file_id" = String, Path, description = "File ID (the shared item itself, or a file inside the shared folder's subtree)") ), responses( (status = 200, description = "File content (or 206 for Range request)"), diff --git a/tests/api/public_shares.hurl b/tests/api/public_shares.hurl index 641f2073..3bc8cdb0 100644 --- a/tests/api/public_shares.hurl +++ b/tests/api/public_shares.hurl @@ -197,6 +197,24 @@ HTTP 200 jsonpath "$.item_type" == "file" +# The public landing page's inline media preview streams the shared file +# through /api/s/{token}/file/{file_id} — the requested file IS the shared +# item here, so the AuthZ gate must accept it (Range-aware 200, inline +# disposition so