mirror of
https://github.com/pgsty/minio.git
synced 2026-09-15 23:14:04 +03:00
03027727d1
Carry the already-parsed source tagging timestamp through the KMS options constructor so replica COPY can apply newer tag updates on explicitly or automatically encrypted destinations. Cover all option encryption modes and signed COPY persistence for newer, stale, duplicate and timestamp-less updates, including bucket defaults. Preserve the real Opus 5.0/max plan review, consensus and local validation. Signed-off-by: Feng Ruohang <rh@vonng.com>
145 lines
6.5 KiB
Go
145 lines
6.5 KiB
Go
// Copyright (c) 2015-2025 MinIO, Inc.
|
|
// Copyright (c) 2025-2026 PGSTY
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/minio/internal/crypto"
|
|
xhttp "github.com/minio/minio/internal/http"
|
|
"github.com/minio/minio/internal/kms"
|
|
)
|
|
|
|
// TestAPICopyObjectReplicaTaggingTimestampUnderKMS covers signed replica COPY
|
|
// requests through encryption, metadata replacement and disk persistence. Both
|
|
// the single-disk and 16-disk fixtures are single-pool backends.
|
|
func TestAPICopyObjectReplicaTaggingTimestampUnderKMS(t *testing.T) {
|
|
defer DetectTestLeak(t)()
|
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPICopyObjectReplicaTaggingTimestampUnderKMS})
|
|
}
|
|
|
|
func testAPICopyObjectReplicaTaggingTimestampUnderKMS(obj ObjectLayer, instance, bucket string, router http.Handler, creds auth.Credentials, t *testing.T) {
|
|
// Ignore the host free-space percentage while retaining real disk I/O.
|
|
for _, pool := range obj.(*erasureServerPools).serverPools {
|
|
for _, set := range pool.sets {
|
|
original := set.getDisks
|
|
disks := append([]StorageAPI(nil), original()...)
|
|
for i := range disks {
|
|
disks[i] = tagTestCapacityDisk{StorageAPI: disks[i]}
|
|
}
|
|
set.getDisks = func() []StorageAPI { return disks }
|
|
defer func() { set.getDisks = original }()
|
|
}
|
|
}
|
|
oldKMS, oldAuto := GlobalKMS, globalAutoEncryption
|
|
GlobalKMS = kms.NewStub("replica-tags-key")
|
|
globalAutoEncryption = false
|
|
defer func() { GlobalKMS, globalAutoEncryption = oldKMS, oldAuto }()
|
|
if _, err := globalBucketMetadataSys.Update(t.Context(), bucket, bucketVersioningConfig, enabledBucketVersioningConfig); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const tsKey = ReservedMetadataPrefixLower + TaggingTimestamp
|
|
stamp := time.Date(2026, 9, 15, 1, 0, 0, 123456789, time.UTC)
|
|
for _, mode := range []string{"none", "explicit-sse-s3", "explicit-kms", "auto-kms", "bucket-kms"} {
|
|
t.Run(instance+"/"+mode, func(t *testing.T) {
|
|
globalAutoEncryption = mode == "auto-kms"
|
|
if mode == "bucket-kms" {
|
|
sseXML := []byte(`<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><ApplyServerSideEncryptionByDefault><SSEAlgorithm>aws:kms</SSEAlgorithm><KMSMasterKeyID>replica-tags-key</KMSMasterKeyID></ApplyServerSideEncryptionByDefault></Rule></ServerSideEncryptionConfiguration>`)
|
|
if _, err := globalBucketMetadataSys.Update(t.Context(), bucket, bucketSSEConfig, sseXML); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
const data = "encrypted replica copy remains readable"
|
|
oi, err := obj.PutObject(t.Context(), bucket, mode, mustGetPutObjReader(t, bytes.NewReader([]byte(data)), int64(len(data)), "", ""), ObjectOptions{
|
|
Versioned: true, UserDefined: map[string]string{xhttp.AmzObjectTagging: "key=old", tsKey: stamp.Format(time.RFC3339Nano)},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, event := range []struct {
|
|
name, tags, wantTags string
|
|
delta, wantDelta time.Duration
|
|
missingTimestamp bool
|
|
}{
|
|
{"newer", "key=new", "key=new", 2, 2, false},
|
|
{"stale", "key=stale", "key=new", 1, 2, false},
|
|
{"duplicate", "key=new", "key=new", 2, 2, false},
|
|
{"newer-again", "key=latest", "key=latest", 3, 3, false},
|
|
{"missing-timestamp", "key=unordered", "key=latest", 0, 3, true},
|
|
} {
|
|
headers := map[string]string{
|
|
xhttp.AmzCopySource: "/" + bucket + "/" + mode + "?versionId=" + oi.VersionID,
|
|
xhttp.AmzMetadataDirective: "REPLACE", xhttp.AmzTagDirective: "REPLACE",
|
|
xhttp.AmzObjectTagging: event.tags, xhttp.MinIOSourceReplicationRequest: "true",
|
|
xhttp.AmzBucketReplicationStatus: "REPLICA", xhttp.MinIOSourceTaggingTimestamp: stamp.Add(event.delta).Format(time.RFC3339Nano),
|
|
xhttp.MinIOSourceMTime: oi.ModTime.Format(time.RFC3339Nano), xhttp.MinIOSourceETag: oi.ETag,
|
|
}
|
|
if event.missingTimestamp {
|
|
delete(headers, xhttp.MinIOSourceTaggingTimestamp)
|
|
}
|
|
if mode == "explicit-sse-s3" {
|
|
headers[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
|
|
}
|
|
if mode == "explicit-kms" {
|
|
headers[xhttp.AmzServerSideEncryption] = "aws:kms"
|
|
headers[xhttp.AmzServerSideEncryptionKmsID] = "replica-tags-key"
|
|
}
|
|
req, err := newTestSignedRequestV4(http.MethodPut, "/"+bucket+"/"+mode+"?versionId="+oi.VersionID, 0, nil, creds.AccessKey, creds.SecretKey, headers)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("%s: COPY %d %s", event.name, w.Code, w.Body.String())
|
|
}
|
|
got, err := obj.GetObjectInfo(t.Context(), bucket, mode, ObjectOptions{VersionID: oi.VersionID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Logf("%s: tags=%q timestamp=%q kms=%v", event.name, got.UserTags, got.UserDefined[tsKey], crypto.S3KMS.IsEncrypted(got.UserDefined))
|
|
if got.UserTags != event.wantTags || got.UserDefined[tsKey] != stamp.Add(event.wantDelta).Format(time.RFC3339Nano) {
|
|
t.Errorf("%s: incorrect persisted tags/timestamp", event.name)
|
|
}
|
|
wantKMS := mode == "explicit-kms" || mode == "auto-kms" || mode == "bucket-kms"
|
|
if crypto.S3KMS.IsEncrypted(got.UserDefined) != wantKMS || crypto.S3.IsEncrypted(got.UserDefined) != (mode == "explicit-sse-s3") {
|
|
t.Errorf("%s: unexpected destination encryption", event.name)
|
|
}
|
|
if got.VersionID != oi.VersionID {
|
|
t.Errorf("%s: version=%q, want %q", event.name, got.VersionID, oi.VersionID)
|
|
}
|
|
req, err = newTestSignedRequestV4(http.MethodGet, "/"+bucket+"/"+mode+"?versionId="+oi.VersionID, 0, nil, creds.AccessKey, creds.SecretKey, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK || w.Body.String() != data {
|
|
t.Fatalf("%s: GET %d %q", event.name, w.Code, w.Body.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|