mirror of
https://github.com/pgsty/minio.git
synced 2026-10-31 12:52:27 +03:00
fix: forward the legal hold as an Object Lock header on federated CopyObject
A cross-deployment CopyObject that requests `x-amz-object-lock-legal-hold: ON` answered 200 while the destination carried no hold. The resolved value reached the remote as ordinary user metadata, `X-Amz-Meta-X-Amz-Object-Lock-Legal-Hold`, so nothing applied it. Retention requested on the same copy survived, which is what made the loss easy to miss. The federation branch passes the resolved metadata map straight to `Core.PutObject` as `PutObjectOptions.UserMetadata`. minio-go's `Header()` writes the typed lock fields first, then prefixes every UserMetadata key it does not recognise with `x-amz-meta-`; `supportedHeaders` covers `x-amz-object-lock-mode` and `x-amz-object-lock-retain-until-date` but not `x-amz-object-lock-legal-hold`, and `isAmzHeader` does not match it either. Retention therefore arrives as real headers and the hold does not. The high-level `validate()` that would have rejected the key never runs, because `Core.PutObject` goes straight to the low-level PUT. Carry the hold on the typed `LegalHold` option and forward a cloned map with the raw key removed. The clone matters twice: typed fields are written before the UserMetadata loop, so a leftover raw key would add a bogus `x-amz-meta-` entry beside the correct header, and the proxy's own response and event metadata are rebuilt from the resolved values rather than the forwarding map, which no longer carries the hold. Retention stays in the map deliberately. It already passes through as a standard header, and moving it to the typed `RetainUntilDate` field would format with `time.RFC3339` and truncate a retain-until date to whole seconds. The new test asserts the wire: the remote must receive `X-Amz-Object-Lock-Legal-Hold` and never the `x-amz-meta-` spelling, and the destination version must actually store the hold. It fails without the change with "legal hold forwarded as user metadata [ON]". Fixes #166 Signed-off-by: Ayush Sharma <72848455+Aeirx@users.noreply.github.com>
This commit is contained in:
+30
-2
@@ -1945,11 +1945,36 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
delete(srcInfo.UserDefined, k)
|
||||
}
|
||||
}
|
||||
// Legal hold does not survive the metadata map. minio-go's Header()
|
||||
// writes the typed lock fields first, then prefixes every UserMetadata
|
||||
// key it does not recognise with "x-amz-meta-": supportedHeaders lists
|
||||
// x-amz-object-lock-mode and x-amz-object-lock-retain-until-date but not
|
||||
// x-amz-object-lock-legal-hold, and isAmzHeader does not match it
|
||||
// either. Forwarded in the map the hold arrives as
|
||||
// X-Amz-Meta-X-Amz-Object-Lock-Legal-Hold, the destination stores no
|
||||
// hold, and the copy still answers 200 (#166).
|
||||
//
|
||||
// Carry only the hold on the typed field, and forward a clone without
|
||||
// the raw key: typed fields are written before the UserMetadata loop, so
|
||||
// a leftover raw key would add a bogus x-amz-meta- entry beside the
|
||||
// correct header. Retention stays in the map -- it already passes
|
||||
// through as a standard header, and moving it to the typed
|
||||
// RetainUntilDate field would truncate the date to whole seconds.
|
||||
legalHoldKey := strings.ToLower(xhttp.AmzObjectLockLegalHold)
|
||||
forwardedLegalHold := srcInfo.UserDefined[legalHoldKey]
|
||||
forwardedMeta := srcInfo.UserDefined
|
||||
if forwardedLegalHold != "" {
|
||||
forwardedMeta = cloneMSS(srcInfo.UserDefined)
|
||||
delete(forwardedMeta, legalHoldKey)
|
||||
}
|
||||
opts := miniogo.PutObjectOptions{
|
||||
UserMetadata: srcInfo.UserDefined,
|
||||
UserMetadata: forwardedMeta,
|
||||
ServerSideEncryption: dstOpts.ServerSideEncryption,
|
||||
UserTags: tag.ToMap(),
|
||||
}
|
||||
if forwardedLegalHold != "" {
|
||||
opts.LegalHold = miniogo.LegalHoldStatus(forwardedLegalHold)
|
||||
}
|
||||
// The destination must carry the same checksum the local path would
|
||||
// produce; the federated path has the remote compute, validate, persist
|
||||
// and return it. Without this the federated copy silently returns an
|
||||
@@ -1996,7 +2021,10 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, rerr), r.URL)
|
||||
return
|
||||
}
|
||||
objInfo.UserDefined = cloneMSS(opts.UserMetadata)
|
||||
// Built from the resolved values rather than the forwarding map: the
|
||||
// legal hold was moved onto the typed option above, so opts.UserMetadata
|
||||
// no longer carries it and the response and event would under-report.
|
||||
objInfo.UserDefined = cloneMSS(srcInfo.UserDefined)
|
||||
// A forwarded checksum header is a request detail, not object metadata.
|
||||
if checksumHeaderValue != "" {
|
||||
delete(objInfo.UserDefined, wantChecksumType.Key())
|
||||
|
||||
Reference in New Issue
Block a user