From f3438b260265f3731291aec62f617bbcbe1a6a13 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Tue, 1 Sep 2026 23:16:04 +0800 Subject: [PATCH] fix: validate CORS state in replication status Count only valid live CORS states in per-site summaries. Treat baselines and tombstones as absent, and diagnose malformed payloads or missing source timestamps.\n\nRefs: #77 Signed-off-by: Feng Ruohang --- cmd/bucket-cors-site-replication_test.go | 38 ++++++++++++++++--- ...site-replication-status-accounting_test.go | 19 +++++----- cmd/site-replication.go | 13 +++---- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/cmd/bucket-cors-site-replication_test.go b/cmd/bucket-cors-site-replication_test.go index 2c0997e98..f8d673903 100644 --- a/cmd/bucket-cors-site-replication_test.go +++ b/cmd/bucket-cors-site-replication_test.go @@ -634,7 +634,7 @@ func testSiteReplicationStatusCountsCorsPerSite(obj ObjectLayer, _ string, bucke globalSiteReplicationSys.Unlock() }() - check := func(name string, wantLocal, wantRemote int) { + check := func(name string, wantLocal, wantRemote int, wantMismatch, wantReplicated bool) { t.Helper() status, err := globalSiteReplicationSys.siteReplicationStatus(ctx, obj, madmin.SRStatusOptions{Buckets: true}) if err != nil { @@ -646,23 +646,51 @@ func testSiteReplicationStatusCountsCorsPerSite(obj ObjectLayer, _ string, bucke if got := status.StatsSummary[remoteID].TotalCorsConfigCount; got != wantRemote { t.Fatalf("%s: remote TotalCorsConfigCount = %d, want %d", name, got, wantRemote) } + for _, id := range []string{localID, remoteID} { + bucketStatus := status.BucketStats[bucket][id] + wantSet := wantLocal != 0 + if id == remoteID { + wantSet = wantRemote != 0 + } + if bucketStatus.HasCorsCfgSet != wantSet { + t.Fatalf("%s: %s HasCorsCfgSet = %v, want %v", name, id, bucketStatus.HasCorsCfgSet, wantSet) + } + if bucketStatus.CorsCfgMismatch != wantMismatch { + t.Fatalf("%s: %s CorsCfgMismatch = %v, want %v", name, id, bucketStatus.CorsCfgMismatch, wantMismatch) + } + gotReplicated := status.StatsSummary[id].ReplicatedCorsConfig != 0 + if gotReplicated != wantReplicated { + t.Fatalf("%s: %s ReplicatedCorsConfig = %d, want replicated %v", name, id, status.StatsSummary[id].ReplicatedCorsConfig, wantReplicated) + } + } } - check("neither site", 0, 0) + check("neither site", 0, 0, false, false) t1 := meta.Created.Add(time.Second) if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, t1); err != nil { t.Fatal(err) } - check("local site only", 1, 0) + check("local site only", 1, 0, true, false) t2 := t1.Add(time.Second) if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, nil, t2); err != nil { t.Fatal(err) } + remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ + Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &encoded, + } + check("live remote without timestamp", 0, 0, true, false) + + invalidXML := base64.StdEncoding.EncodeToString([]byte(`not xml`)) + remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ + Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &invalidXML, CorsConfigUpdatedAt: t2, + } + check("invalid remote XML", 0, 0, true, false) + remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &encoded, CorsConfigUpdatedAt: t2, } - check("remote site only", 0, 1) + check("remote site only", 0, 1, true, false) t3 := t2.Add(time.Second) if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, t3); err != nil { @@ -671,7 +699,7 @@ func testSiteReplicationStatusCountsCorsPerSite(obj ObjectLayer, _ string, bucke remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &encoded, CorsConfigUpdatedAt: t3, } - check("both sites", 1, 1) + check("both sites", 1, 1, false, true) } func TestCORSReplicationStateOrdering(t *testing.T) { diff --git a/cmd/site-replication-status-accounting_test.go b/cmd/site-replication-status-accounting_test.go index ff4deeef1..bebc4b182 100644 --- a/cmd/site-replication-status-accounting_test.go +++ b/cmd/site-replication-status-accounting_test.go @@ -93,15 +93,16 @@ func testSiteReplicationStatusAccountsPerSiteAndSurvivesMalformedConfig(obj Obje CreatedAt: localMeta.Created, }, remoteBucket: { - Bucket: remoteBucket, - CreatedAt: remoteBucketMeta.Created, - Tags: encode(tagXML), - Versioning: encode(versioningXML), - ObjectLockConfig: encode(objectLockXML), - SSEConfig: encode(sseXML), - QuotaConfig: encode(quotaJSON), - Policy: remotePolicy, - CorsConfig: encode([]byte(testSiteReplicationCORSDoc)), + Bucket: remoteBucket, + CreatedAt: remoteBucketMeta.Created, + Tags: encode(tagXML), + Versioning: encode(versioningXML), + ObjectLockConfig: encode(objectLockXML), + SSEConfig: encode(sseXML), + QuotaConfig: encode(quotaJSON), + Policy: remotePolicy, + CorsConfig: encode([]byte(testSiteReplicationCORSDoc)), + CorsConfigUpdatedAt: remoteBucketMeta.Created, }, }, } diff --git a/cmd/site-replication.go b/cmd/site-replication.go index 31232fc4d..399260583 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -3557,13 +3557,12 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O logInvalid("sse", err) } } - if s.CorsConfig != nil { - if _, err := decodeCORSReplicationPayload(s.CorsConfig); err == nil { - validCorsCfg[i] = true - corsCfgCount++ - } else { - logInvalid("cors", err) - } + corsState, err := corsReplicationStateFromInfo(s.SRBucketInfo) + if err != nil { + logInvalid("cors", err) + } else if corsState.kind == corsReplicationLive { + validCorsCfg[i] = true + corsCfgCount++ } ss, ok := info.StatsSummary[s.DeploymentID] if !ok {