Browse Source
Fix get object lock configuration handler (#6996)
Fix get object lock configuration handler (#6996)
* 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 --------- 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>pull/6422/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 842 additions and 312 deletions
-
4test/s3/retention/s3_retention_test.go
-
57weed/admin/dash/admin_server.go
-
46weed/admin/dash/bucket_management.go
-
70weed/s3api/auth_credentials_subscribe.go
-
232weed/s3api/object_lock_utils.go
-
6weed/s3api/s3_constants/extend_key.go
-
37weed/s3api/s3api_bucket_config.go
-
16weed/s3api/s3api_bucket_handlers.go
-
139weed/s3api/s3api_bucket_handlers_object_lock_config.go
-
126weed/s3api/s3api_object_handlers_legal_hold.go
-
103weed/s3api/s3api_object_handlers_put.go
-
222weed/s3api/s3api_object_handlers_retention.go
-
90weed/s3api/s3api_object_lock_fix_test.go
-
6weed/s3api/s3api_server.go
@ -0,0 +1,232 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import ( |
||||
|
"encoding/xml" |
||||
|
"fmt" |
||||
|
"strconv" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
||||
|
) |
||||
|
|
||||
|
// ObjectLockUtils provides shared utilities for Object Lock configuration
|
||||
|
// These functions are used by both Admin UI and S3 API handlers to ensure consistency
|
||||
|
|
||||
|
// VersioningUtils provides shared utilities for bucket versioning configuration
|
||||
|
// These functions ensure Admin UI and S3 API use the same versioning keys
|
||||
|
|
||||
|
// StoreVersioningInExtended stores versioning configuration in entry extended attributes
|
||||
|
func StoreVersioningInExtended(entry *filer_pb.Entry, enabled bool) error { |
||||
|
if entry.Extended == nil { |
||||
|
entry.Extended = make(map[string][]byte) |
||||
|
} |
||||
|
|
||||
|
if enabled { |
||||
|
entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled) |
||||
|
} else { |
||||
|
entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningSuspended) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// LoadVersioningFromExtended loads versioning configuration from entry extended attributes
|
||||
|
func LoadVersioningFromExtended(entry *filer_pb.Entry) (bool, bool) { |
||||
|
if entry == nil || entry.Extended == nil { |
||||
|
return false, false // not found, default to suspended
|
||||
|
} |
||||
|
|
||||
|
// Check for S3 API compatible key
|
||||
|
if versioningBytes, exists := entry.Extended[s3_constants.ExtVersioningKey]; exists { |
||||
|
enabled := string(versioningBytes) == s3_constants.VersioningEnabled |
||||
|
return enabled, true |
||||
|
} |
||||
|
|
||||
|
return false, false // not found
|
||||
|
} |
||||
|
|
||||
|
// CreateObjectLockConfiguration creates a new ObjectLockConfiguration with the specified parameters
|
||||
|
func CreateObjectLockConfiguration(enabled bool, mode string, days int, years int) *ObjectLockConfiguration { |
||||
|
if !enabled { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
config := &ObjectLockConfiguration{ |
||||
|
ObjectLockEnabled: s3_constants.ObjectLockEnabled, |
||||
|
} |
||||
|
|
||||
|
// Add default retention rule if mode and period are specified
|
||||
|
if mode != "" && (days > 0 || years > 0) { |
||||
|
config.Rule = &ObjectLockRule{ |
||||
|
DefaultRetention: &DefaultRetention{ |
||||
|
Mode: mode, |
||||
|
Days: days, |
||||
|
Years: years, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return config |
||||
|
} |
||||
|
|
||||
|
// ObjectLockConfigurationToXML converts ObjectLockConfiguration to XML bytes
|
||||
|
func ObjectLockConfigurationToXML(config *ObjectLockConfiguration) ([]byte, error) { |
||||
|
if config == nil { |
||||
|
return nil, fmt.Errorf("object lock configuration is nil") |
||||
|
} |
||||
|
|
||||
|
return xml.Marshal(config) |
||||
|
} |
||||
|
|
||||
|
// StoreObjectLockConfigurationInExtended stores Object Lock configuration in entry extended attributes
|
||||
|
func StoreObjectLockConfigurationInExtended(entry *filer_pb.Entry, config *ObjectLockConfiguration) error { |
||||
|
if entry.Extended == nil { |
||||
|
entry.Extended = make(map[string][]byte) |
||||
|
} |
||||
|
|
||||
|
if config == nil { |
||||
|
// Remove Object Lock configuration
|
||||
|
delete(entry.Extended, s3_constants.ExtObjectLockEnabledKey) |
||||
|
delete(entry.Extended, s3_constants.ExtObjectLockDefaultModeKey) |
||||
|
delete(entry.Extended, s3_constants.ExtObjectLockDefaultDaysKey) |
||||
|
delete(entry.Extended, s3_constants.ExtObjectLockDefaultYearsKey) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Store the enabled flag
|
||||
|
entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(config.ObjectLockEnabled) |
||||
|
|
||||
|
// Store default retention configuration if present
|
||||
|
if config.Rule != nil && config.Rule.DefaultRetention != nil { |
||||
|
defaultRetention := config.Rule.DefaultRetention |
||||
|
|
||||
|
// Store mode
|
||||
|
if defaultRetention.Mode != "" { |
||||
|
entry.Extended[s3_constants.ExtObjectLockDefaultModeKey] = []byte(defaultRetention.Mode) |
||||
|
} |
||||
|
|
||||
|
// Store days
|
||||
|
if defaultRetention.Days > 0 { |
||||
|
entry.Extended[s3_constants.ExtObjectLockDefaultDaysKey] = []byte(strconv.Itoa(defaultRetention.Days)) |
||||
|
} |
||||
|
|
||||
|
// Store years
|
||||
|
if defaultRetention.Years > 0 { |
||||
|
entry.Extended[s3_constants.ExtObjectLockDefaultYearsKey] = []byte(strconv.Itoa(defaultRetention.Years)) |
||||
|
} |
||||
|
} else { |
||||
|
// Remove default retention if not present
|
||||
|
delete(entry.Extended, s3_constants.ExtObjectLockDefaultModeKey) |
||||
|
delete(entry.Extended, s3_constants.ExtObjectLockDefaultDaysKey) |
||||
|
delete(entry.Extended, s3_constants.ExtObjectLockDefaultYearsKey) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// LoadObjectLockConfigurationFromExtended loads Object Lock configuration from entry extended attributes
|
||||
|
func LoadObjectLockConfigurationFromExtended(entry *filer_pb.Entry) (*ObjectLockConfiguration, bool) { |
||||
|
if entry == nil || entry.Extended == nil { |
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
// Check if Object Lock is enabled
|
||||
|
enabledBytes, exists := entry.Extended[s3_constants.ExtObjectLockEnabledKey] |
||||
|
if !exists { |
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
enabled := string(enabledBytes) |
||||
|
if enabled != s3_constants.ObjectLockEnabled && enabled != "true" { |
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
// Create basic configuration
|
||||
|
config := &ObjectLockConfiguration{ |
||||
|
ObjectLockEnabled: s3_constants.ObjectLockEnabled, |
||||
|
} |
||||
|
|
||||
|
// Load default retention configuration if present
|
||||
|
if modeBytes, exists := entry.Extended[s3_constants.ExtObjectLockDefaultModeKey]; exists { |
||||
|
mode := string(modeBytes) |
||||
|
|
||||
|
// Parse days and years
|
||||
|
var days, years int |
||||
|
if daysBytes, exists := entry.Extended[s3_constants.ExtObjectLockDefaultDaysKey]; exists { |
||||
|
if parsed, err := strconv.Atoi(string(daysBytes)); err == nil { |
||||
|
days = parsed |
||||
|
} |
||||
|
} |
||||
|
if yearsBytes, exists := entry.Extended[s3_constants.ExtObjectLockDefaultYearsKey]; exists { |
||||
|
if parsed, err := strconv.Atoi(string(yearsBytes)); err == nil { |
||||
|
years = parsed |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Create rule if we have a mode and at least days or years
|
||||
|
if mode != "" && (days > 0 || years > 0) { |
||||
|
config.Rule = &ObjectLockRule{ |
||||
|
DefaultRetention: &DefaultRetention{ |
||||
|
Mode: mode, |
||||
|
Days: days, |
||||
|
Years: years, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return config, true |
||||
|
} |
||||
|
|
||||
|
// ExtractObjectLockInfoFromConfig extracts basic Object Lock information from configuration
|
||||
|
// Returns: enabled, mode, duration (for UI display)
|
||||
|
func ExtractObjectLockInfoFromConfig(config *ObjectLockConfiguration) (bool, string, int32) { |
||||
|
if config == nil || config.ObjectLockEnabled != s3_constants.ObjectLockEnabled { |
||||
|
return false, "", 0 |
||||
|
} |
||||
|
|
||||
|
if config.Rule == nil || config.Rule.DefaultRetention == nil { |
||||
|
return true, "", 0 |
||||
|
} |
||||
|
|
||||
|
defaultRetention := config.Rule.DefaultRetention |
||||
|
|
||||
|
// Convert years to days for consistent representation
|
||||
|
days := defaultRetention.Days |
||||
|
if defaultRetention.Years > 0 { |
||||
|
days += defaultRetention.Years * 365 |
||||
|
} |
||||
|
|
||||
|
return true, defaultRetention.Mode, int32(days) |
||||
|
} |
||||
|
|
||||
|
// CreateObjectLockConfigurationFromParams creates ObjectLockConfiguration from individual parameters
|
||||
|
// This is a convenience function for Admin UI usage
|
||||
|
func CreateObjectLockConfigurationFromParams(enabled bool, mode string, duration int32) *ObjectLockConfiguration { |
||||
|
if !enabled { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return CreateObjectLockConfiguration(enabled, mode, int(duration), 0) |
||||
|
} |
||||
|
|
||||
|
// ValidateObjectLockParameters validates Object Lock parameters before creating configuration
|
||||
|
func ValidateObjectLockParameters(enabled bool, mode string, duration int32) error { |
||||
|
if !enabled { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
if mode != s3_constants.RetentionModeGovernance && mode != s3_constants.RetentionModeCompliance { |
||||
|
return ErrInvalidObjectLockMode |
||||
|
} |
||||
|
|
||||
|
if duration <= 0 { |
||||
|
return ErrInvalidObjectLockDuration |
||||
|
} |
||||
|
|
||||
|
if duration > MaxRetentionDays { |
||||
|
return ErrObjectLockDurationExceeded |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
@ -0,0 +1,139 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import ( |
||||
|
"encoding/xml" |
||||
|
"net/http" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/glog" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
||||
|
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats" |
||||
|
) |
||||
|
|
||||
|
// PutObjectLockConfigurationHandler Put object Lock configuration
|
||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLockConfiguration.html
|
||||
|
func (s3a *S3ApiServer) PutObjectLockConfigurationHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
bucket, _ := s3_constants.GetBucketAndObject(r) |
||||
|
glog.V(3).Infof("PutObjectLockConfigurationHandler %s", bucket) |
||||
|
|
||||
|
// Check if Object Lock is available for this bucket (requires versioning)
|
||||
|
if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "PutObjectLockConfigurationHandler") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Parse object lock configuration from request body
|
||||
|
config, err := parseObjectLockConfiguration(r) |
||||
|
if err != nil { |
||||
|
glog.Errorf("PutObjectLockConfigurationHandler: failed to parse object lock config: %v", err) |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Validate object lock configuration
|
||||
|
if err := validateObjectLockConfiguration(config); err != nil { |
||||
|
glog.Errorf("PutObjectLockConfigurationHandler: invalid object lock config: %v", err) |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Set object lock configuration on the bucket
|
||||
|
errCode := s3a.updateBucketConfig(bucket, func(bucketConfig *BucketConfig) error { |
||||
|
// Set the cached Object Lock configuration
|
||||
|
bucketConfig.ObjectLockConfig = config |
||||
|
return nil |
||||
|
}) |
||||
|
|
||||
|
if errCode != s3err.ErrNone { |
||||
|
glog.Errorf("PutObjectLockConfigurationHandler: failed to set object lock config: %v", errCode) |
||||
|
s3err.WriteErrorResponse(w, r, errCode) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Record metrics
|
||||
|
stats_collect.RecordBucketActiveTime(bucket) |
||||
|
|
||||
|
// Return success (HTTP 200 with no body)
|
||||
|
w.WriteHeader(http.StatusOK) |
||||
|
glog.V(3).Infof("PutObjectLockConfigurationHandler: successfully set object lock config for %s", bucket) |
||||
|
} |
||||
|
|
||||
|
// GetObjectLockConfigurationHandler Get object Lock configuration
|
||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html
|
||||
|
func (s3a *S3ApiServer) GetObjectLockConfigurationHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
bucket, _ := s3_constants.GetBucketAndObject(r) |
||||
|
glog.V(3).Infof("GetObjectLockConfigurationHandler %s", bucket) |
||||
|
|
||||
|
// Get bucket configuration
|
||||
|
bucketConfig, errCode := s3a.getBucketConfig(bucket) |
||||
|
if errCode != s3err.ErrNone { |
||||
|
glog.Errorf("GetObjectLockConfigurationHandler: failed to get bucket config: %v", errCode) |
||||
|
s3err.WriteErrorResponse(w, r, errCode) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
var configXML []byte |
||||
|
|
||||
|
// Check if we have cached Object Lock configuration
|
||||
|
if bucketConfig.ObjectLockConfig != nil { |
||||
|
// Use cached configuration and marshal it to XML for response
|
||||
|
marshaledXML, err := xml.Marshal(bucketConfig.ObjectLockConfig) |
||||
|
if err != nil { |
||||
|
glog.Errorf("GetObjectLockConfigurationHandler: failed to marshal cached Object Lock config: %v", err) |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Write XML response
|
||||
|
w.Header().Set("Content-Type", "application/xml") |
||||
|
w.WriteHeader(http.StatusOK) |
||||
|
if _, err := w.Write([]byte(xml.Header)); err != nil { |
||||
|
glog.Errorf("GetObjectLockConfigurationHandler: failed to write XML header: %v", err) |
||||
|
return |
||||
|
} |
||||
|
if _, err := w.Write(marshaledXML); err != nil { |
||||
|
glog.Errorf("GetObjectLockConfigurationHandler: failed to write config XML: %v", err) |
||||
|
return |
||||
|
} |
||||
|
glog.V(3).Infof("GetObjectLockConfigurationHandler: successfully retrieved cached object lock config for %s", bucket) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Fallback: check for legacy storage in extended attributes
|
||||
|
if bucketConfig.Entry.Extended != nil { |
||||
|
// Check if Object Lock is enabled via boolean flag
|
||||
|
if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists { |
||||
|
enabled := string(enabledBytes) |
||||
|
if enabled == s3_constants.ObjectLockEnabled || enabled == "true" { |
||||
|
// Generate minimal XML configuration for enabled Object Lock without retention policies
|
||||
|
minimalConfig := `<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled></ObjectLockConfiguration>` |
||||
|
configXML = []byte(minimalConfig) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If no Object Lock configuration found, return error
|
||||
|
if len(configXML) == 0 { |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLockConfiguration) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Set response headers
|
||||
|
w.Header().Set("Content-Type", "application/xml") |
||||
|
w.WriteHeader(http.StatusOK) |
||||
|
|
||||
|
// Write XML response
|
||||
|
if _, err := w.Write([]byte(xml.Header)); err != nil { |
||||
|
glog.Errorf("GetObjectLockConfigurationHandler: failed to write XML header: %v", err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if _, err := w.Write(configXML); err != nil { |
||||
|
glog.Errorf("GetObjectLockConfigurationHandler: failed to write config XML: %v", err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Record metrics
|
||||
|
stats_collect.RecordBucketActiveTime(bucket) |
||||
|
|
||||
|
glog.V(3).Infof("GetObjectLockConfigurationHandler: successfully retrieved object lock config for %s", bucket) |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import ( |
||||
|
"encoding/xml" |
||||
|
"errors" |
||||
|
"net/http" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/glog" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
||||
|
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats" |
||||
|
) |
||||
|
|
||||
|
// PutObjectLegalHoldHandler Put object Legal Hold
|
||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html
|
||||
|
func (s3a *S3ApiServer) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
bucket, object := s3_constants.GetBucketAndObject(r) |
||||
|
glog.V(3).Infof("PutObjectLegalHoldHandler %s %s", bucket, object) |
||||
|
|
||||
|
// Check if Object Lock is available for this bucket (requires versioning)
|
||||
|
if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "PutObjectLegalHoldHandler") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Get version ID from query parameters
|
||||
|
versionId := r.URL.Query().Get("versionId") |
||||
|
|
||||
|
// Parse legal hold configuration from request body
|
||||
|
legalHold, err := parseObjectLegalHold(r) |
||||
|
if err != nil { |
||||
|
glog.Errorf("PutObjectLegalHoldHandler: failed to parse legal hold config: %v", err) |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Validate legal hold configuration
|
||||
|
if err := validateLegalHold(legalHold); err != nil { |
||||
|
glog.Errorf("PutObjectLegalHoldHandler: invalid legal hold config: %v", err) |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Set legal hold on the object
|
||||
|
if err := s3a.setObjectLegalHold(bucket, object, versionId, legalHold); err != nil { |
||||
|
glog.Errorf("PutObjectLegalHoldHandler: failed to set legal hold: %v", err) |
||||
|
|
||||
|
// Handle specific error cases
|
||||
|
if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) { |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Record metrics
|
||||
|
stats_collect.RecordBucketActiveTime(bucket) |
||||
|
|
||||
|
// Return success (HTTP 200 with no body)
|
||||
|
w.WriteHeader(http.StatusOK) |
||||
|
glog.V(3).Infof("PutObjectLegalHoldHandler: successfully set legal hold for %s/%s", bucket, object) |
||||
|
} |
||||
|
|
||||
|
// GetObjectLegalHoldHandler Get object Legal Hold
|
||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLegalHold.html
|
||||
|
func (s3a *S3ApiServer) GetObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
bucket, object := s3_constants.GetBucketAndObject(r) |
||||
|
glog.V(3).Infof("GetObjectLegalHoldHandler %s %s", bucket, object) |
||||
|
|
||||
|
// Check if Object Lock is available for this bucket (requires versioning)
|
||||
|
if !s3a.handleObjectLockAvailabilityCheck(w, r, bucket, "GetObjectLegalHoldHandler") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Get version ID from query parameters
|
||||
|
versionId := r.URL.Query().Get("versionId") |
||||
|
|
||||
|
// Get legal hold configuration for the object
|
||||
|
legalHold, err := s3a.getObjectLegalHold(bucket, object, versionId) |
||||
|
if err != nil { |
||||
|
glog.Errorf("GetObjectLegalHoldHandler: failed to get legal hold: %v", err) |
||||
|
|
||||
|
// Handle specific error cases
|
||||
|
if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) { |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if errors.Is(err, ErrNoLegalHoldConfiguration) { |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLegalHold) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Marshal legal hold configuration to XML
|
||||
|
legalHoldXML, err := xml.Marshal(legalHold) |
||||
|
if err != nil { |
||||
|
glog.Errorf("GetObjectLegalHoldHandler: failed to marshal legal hold: %v", err) |
||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Set response headers
|
||||
|
w.Header().Set("Content-Type", "application/xml") |
||||
|
w.WriteHeader(http.StatusOK) |
||||
|
|
||||
|
// Write XML response
|
||||
|
if _, err := w.Write([]byte(xml.Header)); err != nil { |
||||
|
glog.Errorf("GetObjectLegalHoldHandler: failed to write XML header: %v", err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if _, err := w.Write(legalHoldXML); err != nil { |
||||
|
glog.Errorf("GetObjectLegalHoldHandler: failed to write legal hold XML: %v", err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Record metrics
|
||||
|
stats_collect.RecordBucketActiveTime(bucket) |
||||
|
|
||||
|
glog.V(3).Infof("GetObjectLegalHoldHandler: successfully retrieved legal hold for %s/%s", bucket, object) |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
||||
|
"github.com/stretchr/testify/assert" |
||||
|
) |
||||
|
|
||||
|
// TestVeeamObjectLockBugFix tests the fix for the bug where GetObjectLockConfigurationHandler
|
||||
|
// would return NoSuchObjectLockConfiguration for buckets with no extended attributes,
|
||||
|
// even when Object Lock was enabled. This caused Veeam to think Object Lock wasn't supported.
|
||||
|
func TestVeeamObjectLockBugFix(t *testing.T) { |
||||
|
|
||||
|
t.Run("Bug case: bucket with no extended attributes", func(t *testing.T) { |
||||
|
// This simulates the bug case where a bucket has no extended attributes at all
|
||||
|
// The old code would immediately return NoSuchObjectLockConfiguration
|
||||
|
// The new code correctly checks if Object Lock is enabled before returning an error
|
||||
|
|
||||
|
bucketConfig := &BucketConfig{ |
||||
|
Name: "test-bucket", |
||||
|
Entry: &filer_pb.Entry{ |
||||
|
Name: "test-bucket", |
||||
|
Extended: nil, // This is the key - no extended attributes
|
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Simulate the isObjectLockEnabledForBucket logic
|
||||
|
enabled := false |
||||
|
if bucketConfig.Entry.Extended != nil { |
||||
|
if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists { |
||||
|
enabled = string(enabledBytes) == s3_constants.ObjectLockEnabled || string(enabledBytes) == "true" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Should correctly return false (not enabled) - this would trigger 404 correctly
|
||||
|
assert.False(t, enabled, "Object Lock should not be enabled when no extended attributes exist") |
||||
|
}) |
||||
|
|
||||
|
t.Run("Fix verification: bucket with Object Lock enabled via boolean flag", func(t *testing.T) { |
||||
|
// This verifies the fix works when Object Lock is enabled via boolean flag
|
||||
|
|
||||
|
bucketConfig := &BucketConfig{ |
||||
|
Name: "test-bucket", |
||||
|
Entry: &filer_pb.Entry{ |
||||
|
Name: "test-bucket", |
||||
|
Extended: map[string][]byte{ |
||||
|
s3_constants.ExtObjectLockEnabledKey: []byte("true"), |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Simulate the isObjectLockEnabledForBucket logic
|
||||
|
enabled := false |
||||
|
if bucketConfig.Entry.Extended != nil { |
||||
|
if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists { |
||||
|
enabled = string(enabledBytes) == s3_constants.ObjectLockEnabled || string(enabledBytes) == "true" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Should correctly return true (enabled) - this would generate minimal XML response
|
||||
|
assert.True(t, enabled, "Object Lock should be enabled when boolean flag is set") |
||||
|
}) |
||||
|
|
||||
|
t.Run("Fix verification: bucket with Object Lock enabled via Enabled constant", func(t *testing.T) { |
||||
|
// Test using the s3_constants.ObjectLockEnabled constant
|
||||
|
|
||||
|
bucketConfig := &BucketConfig{ |
||||
|
Name: "test-bucket", |
||||
|
Entry: &filer_pb.Entry{ |
||||
|
Name: "test-bucket", |
||||
|
Extended: map[string][]byte{ |
||||
|
s3_constants.ExtObjectLockEnabledKey: []byte(s3_constants.ObjectLockEnabled), |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Simulate the isObjectLockEnabledForBucket logic
|
||||
|
enabled := false |
||||
|
if bucketConfig.Entry.Extended != nil { |
||||
|
if enabledBytes, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey]; exists { |
||||
|
enabled = string(enabledBytes) == s3_constants.ObjectLockEnabled || string(enabledBytes) == "true" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Should correctly return true (enabled)
|
||||
|
assert.True(t, enabled, "Object Lock should be enabled when constant is used") |
||||
|
}) |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue