mirror of
https://github.com/pgsty/minio.git
synced 2026-09-05 18:16:16 +03:00
feat: replicate per-bucket CORS across sites and harden the protocol path
Site replication emitted SRBucketMetaTypeCorsConfig on PutBucketCors, but
the peer receive/apply, initial-sync, status, and heal paths did not carry
the CORS metadata. Replicated sites could therefore diverge on CORS config
even though the originating request succeeded.
Complete every site-replication path for CORS, mirroring the SSEConfig
pattern:
- peer apply: PeerBucketCorsConfigHandler + item.Cors handling in
PeerBucketMetadataUpdateHandler, with an updatedAt staleness guard
- initial sync: push existing CorsConfigXML via BucketMetaHook
- status: parse per-site CorsConfig, count/compare, surface
CorsCfgMismatch/HasCorsCfgSet/ReplicatedCorsConfig, and include CORS in
the bucket-stats aggregation filter
- heal: healCORSMetadata, including nil -> delete propagation
Also harden the request/config path:
- PutBucketCors validates the supplied Content-MD5/checksum via
validateLengthAndChecksum
- CORS validation rejects more than one wildcard per AllowedOrigin/
AllowedHeader and enforces the 255-char rule ID limit
- preflight responses Vary on Origin, Access-Control-Request-Method, and
Access-Control-Request-Headers
Add focused tests for the CORS SR transport round-trip, the metadata
equality helper, and the new validation constraints.
Signed-off-by: h5vx <h5v@protonmail.com>
This commit is contained in:
@@ -4037,6 +4037,7 @@
|
|||||||
"cmd:cmd:method:SiteReplicationSys.Netperf",
|
"cmd:cmd:method:SiteReplicationSys.Netperf",
|
||||||
"cmd:cmd:method:SiteReplicationSys.PeerAddPolicyHandler",
|
"cmd:cmd:method:SiteReplicationSys.PeerAddPolicyHandler",
|
||||||
"cmd:cmd:method:SiteReplicationSys.PeerBucketConfigureReplHandler",
|
"cmd:cmd:method:SiteReplicationSys.PeerBucketConfigureReplHandler",
|
||||||
|
"cmd:cmd:method:SiteReplicationSys.PeerBucketCorsConfigHandler",
|
||||||
"cmd:cmd:method:SiteReplicationSys.PeerBucketDeleteHandler",
|
"cmd:cmd:method:SiteReplicationSys.PeerBucketDeleteHandler",
|
||||||
"cmd:cmd:method:SiteReplicationSys.PeerBucketLCConfigHandler",
|
"cmd:cmd:method:SiteReplicationSys.PeerBucketLCConfigHandler",
|
||||||
"cmd:cmd:method:SiteReplicationSys.PeerBucketMakeWithVersioningHandler",
|
"cmd:cmd:method:SiteReplicationSys.PeerBucketMakeWithVersioningHandler",
|
||||||
|
|||||||
@@ -258,6 +258,8 @@ func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *ht
|
|||||||
err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig, item.UpdatedAt)
|
err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig, item.UpdatedAt)
|
||||||
case madmin.SRBucketMetaTypeSSEConfig:
|
case madmin.SRBucketMetaTypeSSEConfig:
|
||||||
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
|
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
|
||||||
|
case madmin.SRBucketMetaTypeCorsConfig:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, item.Bucket, item.Cors, item.UpdatedAt)
|
||||||
case madmin.SRBucketMetaLCConfig:
|
case madmin.SRBucketMetaLCConfig:
|
||||||
err = globalSiteReplicationSys.PeerBucketLCConfigHandler(ctx, item.Bucket, item.ExpiryLCConfig, item.UpdatedAt)
|
err = globalSiteReplicationSys.PeerBucketLCConfigHandler(ctx, item.Bucket, item.ExpiryLCConfig, item.UpdatedAt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -679,7 +679,11 @@ func applyBucketCors(w http.ResponseWriter, r *http.Request, cfg *bktcors.Config
|
|||||||
h.Set("Access-Control-Max-Age", strconv.Itoa(rule.MaxAgeSeconds))
|
h.Set("Access-Control-Max-Age", strconv.Itoa(rule.MaxAgeSeconds))
|
||||||
}
|
}
|
||||||
h.Set("Access-Control-Allow-Credentials", "true")
|
h.Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
// A preflight response depends on all three request headers that
|
||||||
|
// determine the outcome, so cache variation must key on each of them.
|
||||||
h.Add("Vary", "Origin")
|
h.Add("Vary", "Origin")
|
||||||
|
h.Add("Vary", "Access-Control-Request-Method")
|
||||||
|
h.Add("Vary", "Access-Control-Request-Headers")
|
||||||
writeResponse(w, http.StatusOK, nil, mimeNone)
|
writeResponse(w, http.StatusOK, nil, mimeNone)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ func (api objectAPIHandlers) PutBucketCorsHandler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutBucketCors requires a Content-Md5 (or a supported trailing/full
|
||||||
|
// checksum). validateLengthAndChecksum wraps r.Body so the supplied
|
||||||
|
// digest is verified as the body is read below.
|
||||||
|
if !validateLengthAndChecksum(r) {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
corsBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
|
corsBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
|||||||
+163
-1
@@ -1632,6 +1632,15 @@ func (c *SiteReplicationSys) PeerBucketMetadataUpdateHandler(ctx context.Context
|
|||||||
meta.QuotaConfigUpdatedAt = item.UpdatedAt
|
meta.QuotaConfigUpdatedAt = item.UpdatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.Cors != nil {
|
||||||
|
configData, err := base64.StdEncoding.DecodeString(*item.Cors)
|
||||||
|
if err != nil {
|
||||||
|
return wrapSRErr(err)
|
||||||
|
}
|
||||||
|
meta.CorsConfigXML = configData
|
||||||
|
meta.CorsConfigUpdatedAt = item.UpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
return globalBucketMetadataSys.save(ctx, meta)
|
return globalBucketMetadataSys.save(ctx, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1749,6 +1758,35 @@ func (c *SiteReplicationSys) PeerBucketSSEConfigHandler(ctx context.Context, buc
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerBucketCorsConfigHandler - copies/deletes CORS config to local cluster.
|
||||||
|
func (c *SiteReplicationSys) PeerBucketCorsConfigHandler(ctx context.Context, bucket string, corsConfig *string, updatedAt time.Time) error {
|
||||||
|
// skip overwrite if local update is newer than peer update.
|
||||||
|
if !updatedAt.IsZero() {
|
||||||
|
if _, updateTm, err := globalBucketMetadataSys.GetCorsConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if corsConfig != nil {
|
||||||
|
configData, err := base64.StdEncoding.DecodeString(*corsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return wrapSRErr(err)
|
||||||
|
}
|
||||||
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketCorsConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
return wrapSRErr(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete cors config
|
||||||
|
_, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketCorsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return wrapSRErr(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// PeerBucketQuotaConfigHandler - copies/deletes policy to local cluster.
|
// PeerBucketQuotaConfigHandler - copies/deletes policy to local cluster.
|
||||||
func (c *SiteReplicationSys) PeerBucketQuotaConfigHandler(ctx context.Context, bucket string, quota *madmin.BucketQuota, updatedAt time.Time) error {
|
func (c *SiteReplicationSys) PeerBucketQuotaConfigHandler(ctx context.Context, bucket string, quota *madmin.BucketQuota, updatedAt time.Time) error {
|
||||||
// skip overwrite if local update is newer than peer update.
|
// skip overwrite if local update is newer than peer update.
|
||||||
@@ -1950,6 +1988,21 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context, addOpts madmin.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replicate existing bucket CORS settings
|
||||||
|
corsConfigData, tm := meta.CorsConfigXML, meta.CorsConfigUpdatedAt
|
||||||
|
if len(corsConfigData) > 0 {
|
||||||
|
corsConfigStr := base64.StdEncoding.EncodeToString(corsConfigData)
|
||||||
|
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeCorsConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
Cors: &corsConfigStr,
|
||||||
|
UpdatedAt: tm,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errSRBucketMetaError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Replicate existing bucket quotas settings
|
// Replicate existing bucket quotas settings
|
||||||
quotaConfigJSON, tm := meta.QuotaConfigJSON, meta.QuotaConfigUpdatedAt
|
quotaConfigJSON, tm := meta.QuotaConfigJSON, meta.QuotaConfigUpdatedAt
|
||||||
if len(quotaConfigJSON) > 0 {
|
if len(quotaConfigJSON) > 0 {
|
||||||
@@ -2720,6 +2773,7 @@ func (c *SiteReplicationSys) SiteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
st.VersioningConfigMismatch ||
|
st.VersioningConfigMismatch ||
|
||||||
st.OLockConfigMismatch ||
|
st.OLockConfigMismatch ||
|
||||||
st.SSEConfigMismatch ||
|
st.SSEConfigMismatch ||
|
||||||
|
st.CorsCfgMismatch ||
|
||||||
st.PolicyMismatch ||
|
st.PolicyMismatch ||
|
||||||
st.ReplicationCfgMismatch ||
|
st.ReplicationCfgMismatch ||
|
||||||
st.QuotaCfgMismatch ||
|
st.QuotaCfgMismatch ||
|
||||||
@@ -3144,8 +3198,9 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
replCfgs := make([]*sreplication.Config, numSites)
|
replCfgs := make([]*sreplication.Config, numSites)
|
||||||
quotaCfgs := make([]*madmin.BucketQuota, numSites)
|
quotaCfgs := make([]*madmin.BucketQuota, numSites)
|
||||||
sseCfgSet := set.NewStringSet()
|
sseCfgSet := set.NewStringSet()
|
||||||
|
corsCfgSet := set.NewStringSet()
|
||||||
versionCfgSet := set.NewStringSet()
|
versionCfgSet := set.NewStringSet()
|
||||||
var tagCount, olockCfgCount, sseCfgCount, versionCfgCount int
|
var tagCount, olockCfgCount, sseCfgCount, corsCfgCount, versionCfgCount int
|
||||||
for i, s := range slc {
|
for i, s := range slc {
|
||||||
if s.ReplicationConfig != nil {
|
if s.ReplicationConfig != nil {
|
||||||
cfgBytes, err := base64.StdEncoding.DecodeString(*s.ReplicationConfig)
|
cfgBytes, err := base64.StdEncoding.DecodeString(*s.ReplicationConfig)
|
||||||
@@ -3216,6 +3271,16 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
sseCfgSet.Add(string(configData))
|
sseCfgSet.Add(string(configData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.CorsConfig != nil {
|
||||||
|
configData, err := base64.StdEncoding.DecodeString(*s.CorsConfig)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
corsCfgCount++
|
||||||
|
if !corsCfgSet.Contains(string(configData)) {
|
||||||
|
corsCfgSet.Add(string(configData))
|
||||||
|
}
|
||||||
|
}
|
||||||
ss, ok := info.StatsSummary[s.DeploymentID]
|
ss, ok := info.StatsSummary[s.DeploymentID]
|
||||||
if !ok {
|
if !ok {
|
||||||
ss = madmin.SRSiteSummary{}
|
ss = madmin.SRSiteSummary{}
|
||||||
@@ -3234,6 +3299,9 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
if sseCfgCount > 0 {
|
if sseCfgCount > 0 {
|
||||||
ss.TotalSSEConfigCount++
|
ss.TotalSSEConfigCount++
|
||||||
}
|
}
|
||||||
|
if corsCfgCount > 0 {
|
||||||
|
ss.TotalCorsConfigCount++
|
||||||
|
}
|
||||||
if versionCfgCount > 0 {
|
if versionCfgCount > 0 {
|
||||||
ss.TotalVersioningConfigCount++
|
ss.TotalVersioningConfigCount++
|
||||||
}
|
}
|
||||||
@@ -3245,6 +3313,7 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
tagMismatch := !isReplicated(tagCount, numSites, tagSet)
|
tagMismatch := !isReplicated(tagCount, numSites, tagSet)
|
||||||
olockCfgMismatch := !isReplicated(olockCfgCount, numSites, olockConfigSet)
|
olockCfgMismatch := !isReplicated(olockCfgCount, numSites, olockConfigSet)
|
||||||
sseCfgMismatch := !isReplicated(sseCfgCount, numSites, sseCfgSet)
|
sseCfgMismatch := !isReplicated(sseCfgCount, numSites, sseCfgSet)
|
||||||
|
corsCfgMismatch := !isReplicated(corsCfgCount, numSites, corsCfgSet)
|
||||||
versionCfgMismatch := !isReplicated(versionCfgCount, numSites, versionCfgSet)
|
versionCfgMismatch := !isReplicated(versionCfgCount, numSites, versionCfgSet)
|
||||||
policyMismatch := !isBktPolicyReplicated(numSites, policies)
|
policyMismatch := !isBktPolicyReplicated(numSites, policies)
|
||||||
replCfgMismatch := !isBktReplCfgReplicated(numSites, replCfgs)
|
replCfgMismatch := !isBktReplCfgReplicated(numSites, replCfgs)
|
||||||
@@ -3267,6 +3336,7 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
TagMismatch: tagMismatch,
|
TagMismatch: tagMismatch,
|
||||||
OLockConfigMismatch: olockCfgMismatch,
|
OLockConfigMismatch: olockCfgMismatch,
|
||||||
SSEConfigMismatch: sseCfgMismatch,
|
SSEConfigMismatch: sseCfgMismatch,
|
||||||
|
CorsCfgMismatch: corsCfgMismatch,
|
||||||
VersioningConfigMismatch: versionCfgMismatch,
|
VersioningConfigMismatch: versionCfgMismatch,
|
||||||
PolicyMismatch: policyMismatch,
|
PolicyMismatch: policyMismatch,
|
||||||
ReplicationCfgMismatch: replCfgMismatch,
|
ReplicationCfgMismatch: replCfgMismatch,
|
||||||
@@ -3277,6 +3347,7 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
HasPolicySet: s.Policy != nil,
|
HasPolicySet: s.Policy != nil,
|
||||||
HasQuotaCfgSet: quotaCfgSet,
|
HasQuotaCfgSet: quotaCfgSet,
|
||||||
HasSSECfgSet: s.SSEConfig != nil,
|
HasSSECfgSet: s.SSEConfig != nil,
|
||||||
|
HasCorsCfgSet: s.CorsConfig != nil,
|
||||||
}
|
}
|
||||||
var m srBucketMetaInfo
|
var m srBucketMetaInfo
|
||||||
if len(bucketStats[s.Bucket]) > dIdx {
|
if len(bucketStats[s.Bucket]) > dIdx {
|
||||||
@@ -3299,6 +3370,9 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
|||||||
if !sseCfgMismatch && sseCfgCount == numSites {
|
if !sseCfgMismatch && sseCfgCount == numSites {
|
||||||
sum.ReplicatedSSEConfig++
|
sum.ReplicatedSSEConfig++
|
||||||
}
|
}
|
||||||
|
if !corsCfgMismatch && corsCfgCount == numSites {
|
||||||
|
sum.ReplicatedCorsConfig++
|
||||||
|
}
|
||||||
if !policyMismatch && len(policies) == numSites {
|
if !policyMismatch && len(policies) == numSites {
|
||||||
sum.ReplicatedBucketPolicies++
|
sum.ReplicatedBucketPolicies++
|
||||||
}
|
}
|
||||||
@@ -3709,6 +3783,12 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI
|
|||||||
bms.SSEConfigUpdatedAt = meta.EncryptionConfigUpdatedAt
|
bms.SSEConfigUpdatedAt = meta.EncryptionConfigUpdatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(meta.CorsConfigXML) > 0 {
|
||||||
|
corsConfigStr := base64.StdEncoding.EncodeToString(meta.CorsConfigXML)
|
||||||
|
bms.CorsConfig = &corsConfigStr
|
||||||
|
bms.CorsConfigUpdatedAt = meta.CorsConfigUpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
if len(meta.ReplicationConfigXML) > 0 {
|
if len(meta.ReplicationConfigXML) > 0 {
|
||||||
rcfgXMLStr := base64.StdEncoding.EncodeToString(meta.ReplicationConfigXML)
|
rcfgXMLStr := base64.StdEncoding.EncodeToString(meta.ReplicationConfigXML)
|
||||||
bms.ReplicationConfig = &rcfgXMLStr
|
bms.ReplicationConfig = &rcfgXMLStr
|
||||||
@@ -4459,6 +4539,7 @@ func (c *SiteReplicationSys) healBuckets(ctx context.Context, objAPI ObjectLayer
|
|||||||
c.healVersioningMetadata(ctx, objAPI, bucket, info)
|
c.healVersioningMetadata(ctx, objAPI, bucket, info)
|
||||||
c.healOLockConfigMetadata(ctx, objAPI, bucket, info)
|
c.healOLockConfigMetadata(ctx, objAPI, bucket, info)
|
||||||
c.healSSEMetadata(ctx, objAPI, bucket, info)
|
c.healSSEMetadata(ctx, objAPI, bucket, info)
|
||||||
|
c.healCORSMetadata(ctx, objAPI, bucket, info)
|
||||||
c.healBucketReplicationConfig(ctx, objAPI, bucket, info, &opts)
|
c.healBucketReplicationConfig(ctx, objAPI, bucket, info, &opts)
|
||||||
c.healBucketPolicies(ctx, objAPI, bucket, info)
|
c.healBucketPolicies(ctx, objAPI, bucket, info)
|
||||||
c.healTagMetadata(ctx, objAPI, bucket, info)
|
c.healTagMetadata(ctx, objAPI, bucket, info)
|
||||||
@@ -4916,6 +4997,87 @@ func (c *SiteReplicationSys) healSSEMetadata(ctx context.Context, objAPI ObjectL
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SiteReplicationSys) healCORSMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
if !c.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
latestID, latestPeerName string
|
||||||
|
lastUpdate time.Time
|
||||||
|
latestCorsConfig *string
|
||||||
|
)
|
||||||
|
|
||||||
|
bs := info.BucketStats[bucket]
|
||||||
|
for dID, ss := range bs {
|
||||||
|
if lastUpdate.IsZero() {
|
||||||
|
lastUpdate = ss.meta.CorsConfigUpdatedAt
|
||||||
|
latestID = dID
|
||||||
|
latestCorsConfig = ss.meta.CorsConfig
|
||||||
|
}
|
||||||
|
// avoid considering just created buckets as latest. Perhaps this site
|
||||||
|
// just joined cluster replication and yet to be sync'd
|
||||||
|
if ss.meta.CreatedAt.Equal(ss.meta.CorsConfigUpdatedAt) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ss.meta.CorsConfigUpdatedAt.After(lastUpdate) {
|
||||||
|
lastUpdate = ss.meta.CorsConfigUpdatedAt
|
||||||
|
latestID = dID
|
||||||
|
latestCorsConfig = ss.meta.CorsConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latestPeerName = info.Sites[latestID].Name
|
||||||
|
var latestCorsConfigBytes []byte
|
||||||
|
var err error
|
||||||
|
if latestCorsConfig != nil {
|
||||||
|
latestCorsConfigBytes, err = base64.StdEncoding.DecodeString(*latestCorsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dID, bStatus := range bs {
|
||||||
|
if !bStatus.CorsCfgMismatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isBucketMetadataEqual(latestCorsConfig, bStatus.meta.CorsConfig) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dID == globalDeploymentID() {
|
||||||
|
if latestCorsConfig == nil {
|
||||||
|
if _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketCorsConfig); err != nil {
|
||||||
|
replLogIf(ctx, fmt.Errorf("Unable to heal CORS metadata from peer site %s : %w", latestPeerName, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketCorsConfig, latestCorsConfigBytes); err != nil {
|
||||||
|
replLogIf(ctx, fmt.Errorf("Unable to heal CORS metadata from peer site %s : %w", latestPeerName, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
admClient, err := c.getAdminClient(ctx, dID)
|
||||||
|
if err != nil {
|
||||||
|
return wrapSRErr(err)
|
||||||
|
}
|
||||||
|
peerName := info.Sites[dID].Name
|
||||||
|
err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeCorsConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
Cors: latestCorsConfig,
|
||||||
|
UpdatedAt: lastUpdate,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||||
|
fmt.Errorf("Unable to heal CORS config metadata for peer %s from peer %s : %w",
|
||||||
|
peerName, latestPeerName, err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
||||||
bs := info.BucketStats[bucket]
|
bs := info.BucketStats[bucket]
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
@@ -66,3 +69,92 @@ func TestGetMissingSiteNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSRBucketMetaCorsRoundTrip verifies that a CORS bucket-meta item
|
||||||
|
// survives the JSON transport used by SRPeerReplicateBucketItem and that
|
||||||
|
// the base64-encoded payload decodes back to the original XML bytes. This
|
||||||
|
// mirrors the initial-sync push, the peer-apply path, and the heal path,
|
||||||
|
// all of which carry the config through SRBucketMeta.Cors as base64.
|
||||||
|
func TestSRBucketMetaCorsRoundTrip(t *testing.T) {
|
||||||
|
const corsXML = `<CORSConfiguration><CORSRule><AllowedOrigin>https://app.example.com</AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`
|
||||||
|
b64 := base64.StdEncoding.EncodeToString([]byte(corsXML))
|
||||||
|
updatedAt := time.Now().UTC().Truncate(time.Second)
|
||||||
|
|
||||||
|
item := madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeCorsConfig,
|
||||||
|
Bucket: "testbucket",
|
||||||
|
Cors: &b64,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(item)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var got madmin.SRBucketMeta
|
||||||
|
if err := json.Unmarshal(data, &got); err != nil {
|
||||||
|
t.Fatalf("unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.Type != madmin.SRBucketMetaTypeCorsConfig {
|
||||||
|
t.Fatalf("type mismatch: got %q", got.Type)
|
||||||
|
}
|
||||||
|
if got.Cors == nil {
|
||||||
|
t.Fatal("expected non-nil Cors after round-trip")
|
||||||
|
}
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(*got.Cors)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(decoded) != corsXML {
|
||||||
|
t.Fatalf("payload mismatch:\n got %q\nwant %q", decoded, corsXML)
|
||||||
|
}
|
||||||
|
if !got.UpdatedAt.Equal(updatedAt) {
|
||||||
|
t.Fatalf("UpdatedAt mismatch: got %v want %v", got.UpdatedAt, updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A deletion is signaled with a nil Cors pointer; it must survive too.
|
||||||
|
del := madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeCorsConfig,
|
||||||
|
Bucket: "testbucket",
|
||||||
|
Cors: nil,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}
|
||||||
|
data, err = json.Marshal(del)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal (delete) failed: %v", err)
|
||||||
|
}
|
||||||
|
var gotDel madmin.SRBucketMeta
|
||||||
|
if err := json.Unmarshal(data, &gotDel); err != nil {
|
||||||
|
t.Fatalf("unmarshal (delete) failed: %v", err)
|
||||||
|
}
|
||||||
|
if gotDel.Cors != nil {
|
||||||
|
t.Fatalf("expected nil Cors for deletion, got %q", *gotDel.Cors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsBucketMetadataEqualCors covers the pointer-comparison helper used by
|
||||||
|
// the CORS heal path to decide whether a peer already holds the latest config.
|
||||||
|
func TestIsBucketMetadataEqualCors(t *testing.T) {
|
||||||
|
a := base64.StdEncoding.EncodeToString([]byte("config-a"))
|
||||||
|
b := base64.StdEncoding.EncodeToString([]byte("config-b"))
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
one *string
|
||||||
|
two *string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"both nil", nil, nil, true},
|
||||||
|
{"one nil", &a, nil, false},
|
||||||
|
{"other nil", nil, &b, false},
|
||||||
|
{"equal", &a, &a, true},
|
||||||
|
{"different", &a, &b, false},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
if got := isBucketMetadataEqual(tc.one, tc.two); got != tc.want {
|
||||||
|
t.Errorf("%s: got %v want %v", tc.name, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ import (
|
|||||||
// maxCORSRules is the maximum number of rules allowed per bucket (AWS S3 limit).
|
// maxCORSRules is the maximum number of rules allowed per bucket (AWS S3 limit).
|
||||||
const maxCORSRules = 100
|
const maxCORSRules = 100
|
||||||
|
|
||||||
|
// maxCORSRuleIDLen is the maximum length of a CORSRule <ID> (AWS S3 limit).
|
||||||
|
const maxCORSRuleIDLen = 255
|
||||||
|
|
||||||
// supportedMethods are the HTTP methods permitted in an AllowedMethod element.
|
// supportedMethods are the HTTP methods permitted in an AllowedMethod element.
|
||||||
var supportedMethods = map[string]bool{
|
var supportedMethods = map[string]bool{
|
||||||
"GET": true,
|
"GET": true,
|
||||||
@@ -74,17 +77,30 @@ func (c *Config) Validate() error {
|
|||||||
return errors.New("CORSConfiguration exceeds the maximum number of rules")
|
return errors.New("CORSConfiguration exceeds the maximum number of rules")
|
||||||
}
|
}
|
||||||
for _, r := range c.CORSRules {
|
for _, r := range c.CORSRules {
|
||||||
|
if len(r.ID) > maxCORSRuleIDLen {
|
||||||
|
return errors.New("CORSRule ID exceeds the maximum length of 255 characters")
|
||||||
|
}
|
||||||
if len(r.AllowedOrigins) == 0 {
|
if len(r.AllowedOrigins) == 0 {
|
||||||
return errors.New("CORSRule must contain at least one AllowedOrigin")
|
return errors.New("CORSRule must contain at least one AllowedOrigin")
|
||||||
}
|
}
|
||||||
if len(r.AllowedMethods) == 0 {
|
if len(r.AllowedMethods) == 0 {
|
||||||
return errors.New("CORSRule must contain at least one AllowedMethod")
|
return errors.New("CORSRule must contain at least one AllowedMethod")
|
||||||
}
|
}
|
||||||
|
for _, o := range r.AllowedOrigins {
|
||||||
|
if strings.Count(o, "*") > 1 {
|
||||||
|
return errors.New("AllowedOrigin may contain at most one wildcard '*': " + o)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, m := range r.AllowedMethods {
|
for _, m := range r.AllowedMethods {
|
||||||
if !supportedMethods[strings.ToUpper(m)] {
|
if !supportedMethods[strings.ToUpper(m)] {
|
||||||
return errors.New("unsupported method in CORSRule: " + m)
|
return errors.New("unsupported method in CORSRule: " + m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, h := range r.AllowedHeaders {
|
||||||
|
if strings.Count(h, "*") > 1 {
|
||||||
|
return errors.New("AllowedHeader may contain at most one wildcard '*': " + h)
|
||||||
|
}
|
||||||
|
}
|
||||||
if r.MaxAgeSeconds < 0 {
|
if r.MaxAgeSeconds < 0 {
|
||||||
return errors.New("MaxAgeSeconds must not be negative")
|
return errors.New("MaxAgeSeconds must not be negative")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,13 @@ func TestParseAndValidate(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateRejections(t *testing.T) {
|
func TestValidateRejections(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"bad method": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>TRACE</AllowedMethod></CORSRule></CORSConfiguration>`,
|
"bad method": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>TRACE</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||||
"no origin": `<CORSConfiguration><CORSRule><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
"no origin": `<CORSConfiguration><CORSRule><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||||
"no method": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin></CORSRule></CORSConfiguration>`,
|
"no method": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin></CORSRule></CORSConfiguration>`,
|
||||||
"negative age": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod><MaxAgeSeconds>-1</MaxAgeSeconds></CORSRule></CORSConfiguration>`,
|
"negative age": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod><MaxAgeSeconds>-1</MaxAgeSeconds></CORSRule></CORSConfiguration>`,
|
||||||
|
"multi wildcard origin": `<CORSConfiguration><CORSRule><AllowedOrigin>https://*.*.example.com</AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||||
|
"multi wildcard header": `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod><AllowedHeader>x-*-*</AllowedHeader></CORSRule></CORSConfiguration>`,
|
||||||
|
"overlong id": `<CORSConfiguration><CORSRule><ID>` + strings.Repeat("a", 256) + `</ID><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`,
|
||||||
}
|
}
|
||||||
for name, doc := range cases {
|
for name, doc := range cases {
|
||||||
c, err := ParseBucketCorsConfig(strings.NewReader(doc))
|
c, err := ParseBucketCorsConfig(strings.NewReader(doc))
|
||||||
|
|||||||
Reference in New Issue
Block a user