fix: return the remote part checksum to federated UploadPartCopy

The legacy etcd federation branch of CopyObjectPartHandler forwards copied
bytes with minio-go Core.PutObjectPart, which can only recover a checksum
from response headers. After the server-side part checksum work, the remote
computes and persists the checksum, but an AWS-compatible UploadPart response
correctly omits a checksum the request did not supply, so the proxy had
nothing to put in CopyPartResult.

The destination now returns the non-empty checksum fields of the PartInfo
produced by that exact write, but only when the request carries the
minio-federated application token that getRemoteInstanceClient already
attaches. Ordinary UploadPart responses are unchanged, and the checksum type
is deliberately not returned because UploadPart does not carry it. The
User-Agent is a response-shape hint only: it never gates authorization,
visibility or validation, and it can expose nothing beyond the checksum of
the body the caller just uploaded.

Reading the checksum from the same PartInfo that produced the response ETag
also keeps the pair bound to one write, so a concurrent overwrite of the same
part number cannot publish another writer's checksum.

Tests cover the application token gating matrix including lookalike tokens,
the real minio-go response parser, concurrent overwrites of one part number,
and an in-process two-deployment probe that drives the federation branch
through the real getRemoteInstanceClient into a real PutObjectPartHandler for
both FULL_OBJECT and COMPOSITE uploads.

Fixes #64

Signed-off-by: Feng Ruohang <rh@vonng.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-25 08:19:52 +08:00
parent f2520f3346
commit ece57a211a
3 changed files with 421 additions and 1 deletions
+47
View File
@@ -55,6 +55,46 @@ import (
// Multipart objectAPIHandlers
// isFederatedInternalRequest reports whether User-Agent carries the minio-go
// application token attached by getRemoteInstanceClient.
//
// This is only a response-shape hint. User-Agent is not authenticated and must
// never gate authorization, object visibility, or request validation. It is
// safe here because the only effect is returning the checksum of the body the
// caller was already authorized to upload.
func isFederatedInternalRequest(userAgent string) bool {
for _, product := range strings.Fields(userAgent) {
name, version, ok := strings.Cut(product, "/")
if ok && name == federatedInternalAppName && version != "" {
return true
}
}
return false
}
// partChecksumMap returns the non-empty part checksums in the form expected by
// hash.AddChecksumHeader. x-amz-checksum-type is deliberately excluded because
// UploadPart does not return it and minio-go cannot carry it in ObjectPart.
func partChecksumMap(partInfo PartInfo) map[string]string {
checksums := make(map[string]string, 1)
if partInfo.ChecksumCRC32 != "" {
checksums[hash.ChecksumCRC32.String()] = partInfo.ChecksumCRC32
}
if partInfo.ChecksumCRC32C != "" {
checksums[hash.ChecksumCRC32C.String()] = partInfo.ChecksumCRC32C
}
if partInfo.ChecksumSHA1 != "" {
checksums[hash.ChecksumSHA1.String()] = partInfo.ChecksumSHA1
}
if partInfo.ChecksumSHA256 != "" {
checksums[hash.ChecksumSHA256.String()] = partInfo.ChecksumSHA256
}
if partInfo.ChecksumCRC64NVME != "" {
checksums[hash.ChecksumCRC64NVME.String()] = partInfo.ChecksumCRC64NVME
}
return checksums
}
// multipartChecksumType returns the base checksum type recorded when a
// multipart upload was created. The boolean reports whether an algorithm was
// recorded at all.
@@ -996,6 +1036,13 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
// Therefore, we have to set the ETag directly as map entry.
w.Header()[xhttp.ETag] = []string{"\"" + etag + "\""}
hash.TransferChecksumHeader(w, r)
if isFederatedInternalRequest(r.UserAgent()) {
// Legacy federation proxies UploadPartCopy through minio-go
// Core.PutObjectPart, which can only recover checksums from response
// headers. Use the PartInfo returned by this exact write so the ETag and
// checksum cannot be mixed with a concurrent overwrite.
hash.AddChecksumHeader(w, partChecksumMap(partInfo))
}
writeSuccessResponseHeadersOnly(w)
}