fix(federation): bind copy timestamps to committed writes

Return the committed object or part time on federation write responses and
capture it for CopyObject and UploadPartCopy without a follow-up read.
Keep successful writes compatible with targets that do not supply a time.

Reject authenticated raw SSE-C replica CopyObject across deployments before
forwarding. Extend the existing SSE fixtures to verify empty objects,
stored checksums, KMS contexts and multipart sources.

Refs: #169, #168, #171
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-12 01:44:22 +08:00
parent 12f631b502
commit f175e98c34
9 changed files with 603 additions and 83 deletions
+38
View File
@@ -36,6 +36,44 @@ import (
var etagRegex = regexp.MustCompile("\"*?([^\"]*?)\"*?$")
// federatedLastModified carries the committed object/part time on the write
// response. HTTP Date is a response time and Last-Modified loses subsecond
// precision, so neither can supply CopyObject/UploadPartCopy's timestamp.
const federatedLastModified = "X-Minio-Last-Modified"
type federatedWriteTimeKey struct{}
// withFederatedWriteTime allocates one result per SDK operation. SDK retries
// run sequentially with this context; concurrent copies never share the result.
func withFederatedWriteTime(ctx context.Context) (context.Context, *time.Time) {
modified := new(time.Time)
return context.WithValue(ctx, federatedWriteTimeKey{}, modified), modified
}
type federatedWriteTransport struct {
http.RoundTripper
}
func (t federatedWriteTransport) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := t.RoundTripper.RoundTrip(r)
if modified, ok := r.Context().Value(federatedWriteTimeKey{}).(*time.Time); ok && r.Method == http.MethodPut {
// Replace on every attempt, including a success from an older target
// without the header. Never retain a failed attempt's timestamp or fail
// a committed write just because its timestamp is unavailable.
*modified = time.Time{}
if err == nil && resp != nil && resp.StatusCode == http.StatusOK {
*modified, _ = time.Parse(time.RFC3339Nano, resp.Header.Get(federatedLastModified))
}
}
return resp, err
}
func setFederatedWriteTime(w http.ResponseWriter, r *http.Request, modified time.Time) {
if isFederatedInternalRequest(r.UserAgent()) && !modified.IsZero() {
w.Header().Set(federatedLastModified, modified.UTC().Format(time.RFC3339Nano))
}
}
// Validates the preconditions for CopyObjectPart, returns true if CopyObjectPart
// operation should not proceed. Preconditions supported are:
//