fix: recognize timestamp-only retention-removal tombstone in resend compare

retentionRemovedAtSource only recognized representation (1) of a removed
retention: the object lock key present with an empty value. But a removal
that arrived by replication persists representation (2): restoreRetention
(and the receiver's replica update path) writes only the retention ordering
timestamp when the mode is empty, leaving the mode and retain-until-date keys
absent. For that shape the helper returned false, so replicationActionForTarget
skipped the GetObjectRetention confirmation and let getReplicationAction's
replicateNone stand, silently dropping a needed removal when the destination
HEAD hides retention behind a permission-filtered credential.

Recognize representation (2) as well: a present retention ordering timestamp
with the mode value absent or empty is a removal. A present timestamp paired
with a non-empty mode is a retention that was set, not removed, and still
returns false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7qJqWwy8oFA6aCXWRzXQe
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-06 11:48:57 +08:00
parent c185635b43
commit 7935c84f9a
2 changed files with 88 additions and 2 deletions
+16 -2
View File
@@ -1067,15 +1067,29 @@ type objectRetentionGetter interface {
GetObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (*minio.RetentionMode, *time.Time, error)
}
// retentionRemovedAtSource reports whether oi carries the shape a removed retention leaves behind,
// an object lock key present with an empty value (cmd/object-handlers.go:3211-3215).
// retentionRemovedAtSource reports whether oi carries the shape a removed retention leaves behind.
// Two representations persist. A retention removed directly on this cluster keeps the object lock
// keys present with empty values (PutObjectRetentionHandler, cmd/object-handlers.go:3309-3316). A
// removal that arrived by replication keeps only the retention ordering timestamp, with the mode
// and retain-until-date keys absent, because restoreRetention and the replica update path write
// the timestamp alone when the mode is empty (cmd/bucket-object-lock.go:388-399,
// cmd/object-handlers.go:1782-1797). A present ordering timestamp paired with a non-empty mode is
// a retention that was set, not removed, and must not be mistaken for one.
func retentionRemovedAtSource(oi ObjectInfo) bool {
lkMap := caseInsensitiveMap(oi.UserDefined)
// Representation (1): an object lock key is present with an empty value.
for _, k := range []string{xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate} {
if v, ok := lkMap.Lookup(k); ok && v == "" {
return true
}
}
// Representation (2): a recorded retention ordering timestamp with the mode value absent or
// empty is a removal restoreRetention persisted without the empty public keys.
if _, ok := oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
if v, ok := lkMap.Lookup(xhttp.AmzObjectLockMode); !ok || v == "" {
return true
}
}
return false
}