fix: keep plain Enabled versioning when Object Lock is enabled on a bucket

Site adoption and ForceCreate preserved a suspended or prefix-excluded
versioning configuration while bootstrapping Object Lock, persisting a state
that PutBucketVersioning itself rejects: objects under an excluded prefix in
a WORM bucket were not versioned and escaped retention. enablePeerBucketVersioning
now takes the lock intent and replaces such configurations with plain Enabled
versioning, and metadata loading ignores prefix exclusions on a locked bucket
as it ignored suspension before. The adoption tests assert the normalized
state and keep the timestamp-preservation checks on valid documents.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvgysXDmhPBBimCReYtA8q
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-02 14:06:56 +08:00
parent 76195f1c68
commit 3b5de82f5a
4 changed files with 21 additions and 13 deletions
+3 -1
View File
@@ -378,8 +378,10 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa
} }
if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) { if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) {
// A locked bucket needs plain Enabled versioning; suspended or
// prefix-excluded configurations are not honored for it.
config, versioningErr := versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML)) config, versioningErr := versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
if versioningErr != nil || !config.Enabled() { if versioningErr != nil || !config.Enabled() || config.PrefixesExcluded() {
b.VersioningConfigXML = enabledBucketVersioningConfig b.VersioningConfigXML = enabledBucketVersioningConfig
} }
} }
+2 -2
View File
@@ -911,7 +911,7 @@ func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts
meta.SetCreatedAt(opts.CreatedAt) meta.SetCreatedAt(opts.CreatedAt)
} }
if opts.LockEnabled { if opts.LockEnabled {
if err := enablePeerBucketVersioning(&meta); err != nil { if err := enablePeerBucketVersioning(&meta, true); err != nil {
return err return err
} }
if len(meta.ObjectLockConfigXML) == 0 { if len(meta.ObjectLockConfigXML) == 0 {
@@ -920,7 +920,7 @@ func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts
} }
} }
if opts.VersioningEnabled { if opts.VersioningEnabled {
if err := enablePeerBucketVersioning(&meta); err != nil { if err := enablePeerBucketVersioning(&meta, opts.LockEnabled); err != nil {
return err return err
} }
} }
+9 -7
View File
@@ -39,7 +39,9 @@ func testPeerBucketAdoptionPreservesLockAndVersioningConfigs(_ ObjectLayer, inst
_ http.Handler, _ auth.Credentials, t *testing.T, _ http.Handler, _ auth.Credentials, t *testing.T,
) { ) {
objectLockXML := []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`) objectLockXML := []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`)
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`) // A locked bucket carries plain Enabled versioning; adoption must keep the
// existing document and its timestamp rather than rewrite them.
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status></VersioningConfiguration>`)
if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, objectLockXML); err != nil { if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, objectLockXML); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -77,15 +79,15 @@ func TestPeerBucketAdoptionBootstrapsMissingConfigs(t *testing.T) {
}) })
} }
func TestPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(t *testing.T) { func TestPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(t *testing.T) {
defer DetectTestLeak(t)() defer DetectTestLeak(t)()
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
t: t, t: t,
objAPITest: testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock, objAPITest: testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock,
}) })
} }
func testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string, func testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string,
_ http.Handler, _ auth.Credentials, t *testing.T, _ http.Handler, _ auth.Credentials, t *testing.T,
) { ) {
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`) versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`)
@@ -106,8 +108,8 @@ func testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(_ ObjectLay
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(after.VersioningConfigXML, before.VersioningConfigXML) || !after.VersioningConfigUpdatedAt.Equal(before.VersioningConfigUpdatedAt) { if !bytes.Equal(after.VersioningConfigXML, enabledBucketVersioningConfig) || !after.VersioningConfigUpdatedAt.After(before.VersioningConfigUpdatedAt) {
t.Fatalf("%s: custom versioning changed while enabling Object Lock", instanceType) t.Fatalf("%s: prefix-excluded versioning survived enabling Object Lock: %q", instanceType, after.VersioningConfigXML)
} }
if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) { if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) {
t.Fatalf("%s: Object Lock was not bootstrapped", instanceType) t.Fatalf("%s: Object Lock was not bootstrapped", instanceType)
@@ -152,7 +154,7 @@ func TestEnablePeerBucketVersioningRepairsInvalidConfig(t *testing.T) {
meta := newBucketMetadata("bucket") meta := newBucketMetadata("bucket")
meta.Created = time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC) meta.Created = time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC)
meta.VersioningConfigXML = []byte(`<VersioningConfiguration>`) meta.VersioningConfigXML = []byte(`<VersioningConfiguration>`)
if err := enablePeerBucketVersioning(&meta); err != nil { if err := enablePeerBucketVersioning(&meta, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) || meta.VersioningConfigUpdatedAt.IsZero() { if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) || meta.VersioningConfigUpdatedAt.IsZero() {
+7 -3
View File
@@ -889,7 +889,11 @@ func (c *SiteReplicationSys) DeleteBucketHook(ctx context.Context, bucket string
return errors.Unwrap(cerr) return errors.Unwrap(cerr)
} }
func enablePeerBucketVersioning(meta *BucketMetadata) error { // enablePeerBucketVersioning turns versioning on for a bucket that is being
// created or adopted. With lockEnabled, Object Lock requires every object to
// be versioned: the S3 API rejects suspended or prefix-excluded versioning on
// a locked bucket, so such a configuration is replaced rather than preserved.
func enablePeerBucketVersioning(meta *BucketMetadata, lockEnabled bool) error {
if len(meta.VersioningConfigXML) == 0 { if len(meta.VersioningConfigXML) == 0 {
meta.VersioningConfigXML = enabledBucketVersioningConfig meta.VersioningConfigXML = enabledBucketVersioningConfig
if meta.VersioningConfigUpdatedAt.IsZero() { if meta.VersioningConfigUpdatedAt.IsZero() {
@@ -898,7 +902,7 @@ func enablePeerBucketVersioning(meta *BucketMetadata) error {
return nil return nil
} }
config, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML)) config, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML))
if err != nil { if err != nil || (lockEnabled && (config.Suspended() || config.PrefixesExcluded())) {
meta.VersioningConfigXML = enabledBucketVersioningConfig meta.VersioningConfigXML = enabledBucketVersioningConfig
meta.VersioningConfigUpdatedAt = UTCNow() meta.VersioningConfigUpdatedAt = UTCNow()
return nil return nil
@@ -943,7 +947,7 @@ func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Con
} }
meta.SetCreatedAt(opts.CreatedAt) meta.SetCreatedAt(opts.CreatedAt)
if err = enablePeerBucketVersioning(&meta); err != nil { if err = enablePeerBucketVersioning(&meta, opts.LockEnabled || len(meta.ObjectLockConfigXML) != 0); err != nil {
return err return err
} }
if opts.LockEnabled && len(meta.ObjectLockConfigXML) == 0 { if opts.LockEnabled && len(meta.ObjectLockConfigXML) == 0 {