mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-11-08 04:46:13 +08:00
Test object lock and retention (#6997)
* fix GetObjectLockConfigurationHandler * cache and use bucket object lock config * subscribe to bucket configuration changes * increase bucket config cache TTL * refactor * Update weed/s3api/s3api_server.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * avoid duplidated work * rename variable * Update s3api_object_handlers_put.go * fix routing * admin ui and api handler are consistent now * use fields instead of xml * fix test * address comments * Update weed/s3api/s3api_object_handlers_put.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/s3/retention/s3_retention_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update weed/s3api/object_lock_utils.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * change error style * errorf * read entry once * add s3 tests for object lock and retention * use marker * install s3 tests * Update s3tests.yml * Update s3tests.yml * Update s3tests.conf * Update s3tests.conf * address test errors * address test errors With these fixes, the s3-tests should now: ✅ Return InvalidBucketState (409 Conflict) for object lock operations on invalid buckets ✅ Return MalformedXML for invalid retention configurations ✅ Include VersionId in response headers when available ✅ Return proper HTTP status codes (403 Forbidden for retention mode changes) ✅ Handle all object lock validation errors consistently * fixes With these comprehensive fixes, the s3-tests should now: ✅ Return InvalidBucketState (409 Conflict) for object lock operations on invalid buckets ✅ Return InvalidRetentionPeriod for invalid retention periods ✅ Return MalformedXML for malformed retention configurations ✅ Include VersionId in response headers when available ✅ Return proper HTTP status codes for all error conditions ✅ Handle all object lock validation errors consistently The workflow should now pass significantly more object lock tests, bringing SeaweedFS's S3 object lock implementation much closer to AWS S3 compatibility standards. * fixes With these final fixes, the s3-tests should now: ✅ Return MalformedXML for ObjectLockEnabled: 'Disabled' ✅ Return MalformedXML when both Days and Years are specified in retention configuration ✅ Return InvalidBucketState (409 Conflict) when trying to suspend versioning on buckets with object lock enabled ✅ Handle all object lock validation errors consistently with proper error codes * constants and fixes ✅ Return InvalidRetentionPeriod for invalid retention values (0 days, negative years) ✅ Return ObjectLockConfigurationNotFoundError when object lock configuration doesn't exist ✅ Handle all object lock validation errors consistently with proper error codes * fixes ✅ Return MalformedXML when both Days and Years are specified in the same retention configuration ✅ Return 400 (Bad Request) with InvalidRequest when object lock operations are attempted on buckets without object lock enabled ✅ Handle all object lock validation errors consistently with proper error codes * fixes ✅ Return 409 (Conflict) with InvalidBucketState for bucket-level object lock configuration operations on buckets without object lock enabled ✅ Allow increasing retention periods and overriding retention with same/later dates ✅ Only block decreasing retention periods without proper bypass permissions ✅ Handle all object lock validation errors consistently with proper error codes * fixes ✅ Include VersionId in multipart upload completion responses when versioning is enabled ✅ Block retention mode changes (GOVERNANCE ↔ COMPLIANCE) without bypass permissions ✅ Handle all object lock validation errors consistently with proper error codes ✅ Pass the remaining object lock tests * fix tests * fixes * pass tests * fix tests * fixes * add error mapping * Update s3tests.conf * fix test_object_lock_put_obj_lock_invalid_days * fixes * fix many issues * fix test_object_lock_delete_multipart_object_with_legal_hold_on * fix tests * refactor * fix test_object_lock_delete_object_with_retention_and_marker * fix tests * fix tests * fix tests * fix test itself * fix tests * fix test * Update weed/s3api/s3api_object_retention.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * reduce logs * address comments --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -77,20 +77,32 @@ func TestObjectLockValidation(t *testing.T) {
|
||||
require.NoError(t, err, "Setting Object Lock retention should succeed")
|
||||
t.Log(" ✅ Object Lock retention applied successfully")
|
||||
|
||||
// Verify retention is in effect
|
||||
// Verify retention allows simple DELETE (creates delete marker) but blocks version deletion
|
||||
// AWS S3 behavior: Simple DELETE (without version ID) is ALWAYS allowed and creates delete marker
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.Error(t, err, "Object should be protected by retention and cannot be deleted")
|
||||
t.Log(" ✅ Object is properly protected by retention policy")
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker (AWS S3 behavior)")
|
||||
t.Log(" ✅ Simple DELETE succeeded (creates delete marker - correct AWS behavior)")
|
||||
|
||||
// Verify we can read the object (should still work)
|
||||
getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
// Now verify that DELETE with version ID is properly blocked by retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
})
|
||||
require.NoError(t, err, "Reading protected object should still work")
|
||||
require.Error(t, err, "DELETE with version ID should be blocked by COMPLIANCE retention")
|
||||
t.Log(" ✅ Object version is properly protected by retention policy")
|
||||
|
||||
// Verify we can read the object version (should still work)
|
||||
// Note: Need to specify version ID since latest version is now a delete marker
|
||||
getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
})
|
||||
require.NoError(t, err, "Reading protected object version should still work")
|
||||
defer getResp.Body.Close()
|
||||
t.Log(" ✅ Protected object can still be read")
|
||||
|
||||
|
||||
@@ -318,20 +318,29 @@ func TestRetentionModeCompliance(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, types.ObjectLockRetentionModeCompliance, retentionResp.Retention.Mode)
|
||||
|
||||
// Try to delete object with bypass - should still fail (compliance mode)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
BypassGovernanceRetention: aws.Bool(true),
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// Try to delete object without bypass - should also fail
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
|
||||
// Try DELETE with version ID - should fail for COMPLIANCE mode
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
})
|
||||
require.Error(t, err, "DELETE with version ID should be blocked by COMPLIANCE retention")
|
||||
|
||||
// Try DELETE with version ID and bypass - should still fail (COMPLIANCE mode ignores bypass)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
BypassGovernanceRetention: aws.Bool(true),
|
||||
})
|
||||
require.Error(t, err, "COMPLIANCE mode should ignore governance bypass")
|
||||
}
|
||||
|
||||
// TestLegalHoldWorkflow tests legal hold functionality
|
||||
@@ -368,37 +377,48 @@ func TestLegalHoldWorkflow(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, types.ObjectLockLegalHoldStatusOn, legalHoldResp.LegalHold.Status)
|
||||
|
||||
// Try to delete object - should fail due to legal hold
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
|
||||
// Remove legal hold
|
||||
// Try DELETE with version ID - should fail due to legal hold
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
})
|
||||
require.Error(t, err, "DELETE with version ID should be blocked by legal hold")
|
||||
|
||||
// Remove legal hold (must specify version ID since latest version is now delete marker)
|
||||
_, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
LegalHold: &types.ObjectLockLegalHold{
|
||||
Status: types.ObjectLockLegalHoldStatusOff,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify legal hold is off
|
||||
// Verify legal hold is off (must specify version ID)
|
||||
legalHoldResp, err = client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, types.ObjectLockLegalHoldStatusOff, legalHoldResp.LegalHold.Status)
|
||||
|
||||
// Now delete should succeed
|
||||
// Now DELETE with version ID should succeed after legal hold removed
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err, "DELETE with version ID should succeed after legal hold removed")
|
||||
}
|
||||
|
||||
// TestObjectLockConfiguration tests bucket object lock configuration
|
||||
@@ -560,31 +580,41 @@ func TestRetentionAndLegalHoldCombination(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to delete with bypass governance - should still fail due to legal hold
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
|
||||
// Try DELETE with version ID and bypass - should still fail due to legal hold
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
BypassGovernanceRetention: aws.Bool(true),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Error(t, err, "Legal hold should prevent deletion even with governance bypass")
|
||||
|
||||
// Remove legal hold
|
||||
// Remove legal hold (must specify version ID since latest version is now delete marker)
|
||||
_, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
LegalHold: &types.ObjectLockLegalHold{
|
||||
Status: types.ObjectLockLegalHoldStatusOff,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now delete with bypass governance should succeed
|
||||
// Now DELETE with version ID and bypass governance should succeed
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
BypassGovernanceRetention: aws.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err, "DELETE with version ID should succeed after legal hold removed and with governance bypass")
|
||||
}
|
||||
|
||||
// TestExpiredRetention tests that objects can be deleted after retention expires
|
||||
|
||||
@@ -42,17 +42,26 @@ func TestWORMRetentionIntegration(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to delete - should fail due to retention
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
|
||||
// Delete with bypass should succeed
|
||||
// Try DELETE with version ID - should fail due to GOVERNANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
})
|
||||
require.Error(t, err, "DELETE with version ID should be blocked by GOVERNANCE retention")
|
||||
|
||||
// Delete with version ID and bypass should succeed
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: putResp.VersionId,
|
||||
BypassGovernanceRetention: aws.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -316,12 +325,20 @@ func TestRetentionWithMultipartUpload(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to delete - should fail
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
|
||||
// Try DELETE with version ID - should fail due to GOVERNANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
VersionId: completeResp.VersionId,
|
||||
})
|
||||
require.Error(t, err, "DELETE with version ID should be blocked by GOVERNANCE retention")
|
||||
}
|
||||
|
||||
// TestRetentionExtendedAttributes tests that retention uses extended attributes correctly
|
||||
|
||||
Reference in New Issue
Block a user