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>
This commit is contained in:
Chris Lu
2025-07-18 02:19:50 -07:00
committed by GitHub
parent 69553e5ba6
commit c6a22ce43a
14 changed files with 842 additions and 312 deletions

View File

@@ -5,7 +5,6 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -24,6 +23,8 @@ import (
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/wdclient"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/s3api"
)
type AdminServer struct {
@@ -293,20 +294,11 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
var objectLockDuration int32 = 0
if resp.Entry.Extended != nil {
if versioningBytes, exists := resp.Entry.Extended["s3.versioning"]; exists {
versioningEnabled = string(versioningBytes) == "Enabled"
}
if objectLockBytes, exists := resp.Entry.Extended["s3.objectlock"]; exists {
objectLockEnabled = string(objectLockBytes) == "Enabled"
}
if objectLockModeBytes, exists := resp.Entry.Extended["s3.objectlock.mode"]; exists {
objectLockMode = string(objectLockModeBytes)
}
if objectLockDurationBytes, exists := resp.Entry.Extended["s3.objectlock.duration"]; exists {
if duration, err := strconv.ParseInt(string(objectLockDurationBytes), 10, 32); err == nil {
objectLockDuration = int32(duration)
}
}
// Use shared utility to extract versioning information
versioningEnabled = extractVersioningFromEntry(resp.Entry)
// Use shared utility to extract Object Lock information
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(resp.Entry)
}
bucket := S3Bucket{
@@ -379,20 +371,11 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
var objectLockDuration int32 = 0
if bucketResp.Entry.Extended != nil {
if versioningBytes, exists := bucketResp.Entry.Extended["s3.versioning"]; exists {
versioningEnabled = string(versioningBytes) == "Enabled"
}
if objectLockBytes, exists := bucketResp.Entry.Extended["s3.objectlock"]; exists {
objectLockEnabled = string(objectLockBytes) == "Enabled"
}
if objectLockModeBytes, exists := bucketResp.Entry.Extended["s3.objectlock.mode"]; exists {
objectLockMode = string(objectLockModeBytes)
}
if objectLockDurationBytes, exists := bucketResp.Entry.Extended["s3.objectlock.duration"]; exists {
if duration, err := strconv.ParseInt(string(objectLockDurationBytes), 10, 32); err == nil {
objectLockDuration = int32(duration)
}
}
// Use shared utility to extract versioning information
versioningEnabled = extractVersioningFromEntry(bucketResp.Entry)
// Use shared utility to extract Object Lock information
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(bucketResp.Entry)
}
details.Bucket.VersioningEnabled = versioningEnabled
@@ -1502,3 +1485,19 @@ func (s *AdminServer) Shutdown() {
glog.V(1).Infof("Admin server shutdown complete")
}
// Function to extract Object Lock information from bucket entry using shared utilities
func extractObjectLockInfoFromEntry(entry *filer_pb.Entry) (bool, string, int32) {
// Try to load Object Lock configuration using shared utility
if config, found := s3api.LoadObjectLockConfigurationFromExtended(entry); found {
return s3api.ExtractObjectLockInfoFromConfig(config)
}
return false, "", 0
}
// Function to extract versioning information from bucket entry using shared utilities
func extractVersioningFromEntry(entry *filer_pb.Entry) bool {
enabled, _ := s3api.LoadVersioningFromExtended(entry)
return enabled
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api"
)
// S3 Bucket management data structures for templates
@@ -340,32 +341,43 @@ func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes
TtlSec: 0,
}
// Create extended attributes map for versioning and object lock
// Create extended attributes map for versioning
extended := make(map[string][]byte)
if versioningEnabled {
extended["s3.versioning"] = []byte("Enabled")
} else {
extended["s3.versioning"] = []byte("Suspended")
// Create bucket entry
bucketEntry := &filer_pb.Entry{
Name: bucketName,
IsDirectory: true,
Attributes: attributes,
Extended: extended,
Quota: quota,
}
// Handle versioning using shared utilities
if err := s3api.StoreVersioningInExtended(bucketEntry, versioningEnabled); err != nil {
return fmt.Errorf("failed to store versioning configuration: %w", err)
}
// Handle Object Lock configuration using shared utilities
if objectLockEnabled {
extended["s3.objectlock"] = []byte("Enabled")
extended["s3.objectlock.mode"] = []byte(objectLockMode)
extended["s3.objectlock.duration"] = []byte(fmt.Sprintf("%d", objectLockDuration))
} else {
extended["s3.objectlock"] = []byte("Disabled")
// Validate Object Lock parameters
if err := s3api.ValidateObjectLockParameters(objectLockEnabled, objectLockMode, objectLockDuration); err != nil {
return fmt.Errorf("invalid Object Lock parameters: %w", err)
}
// Create Object Lock configuration using shared utility
objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, objectLockMode, objectLockDuration)
// Store Object Lock configuration in extended attributes using shared utility
if err := s3api.StoreObjectLockConfigurationInExtended(bucketEntry, objectLockConfig); err != nil {
return fmt.Errorf("failed to store Object Lock configuration: %w", err)
}
}
// Create bucket directory under /buckets
_, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
Directory: "/buckets",
Entry: &filer_pb.Entry{
Name: bucketName,
IsDirectory: true,
Attributes: attributes,
Extended: extended,
Quota: quota,
},
Entry: bucketEntry,
})
if err != nil {
return fmt.Errorf("failed to create bucket directory: %w", err)

View File

@@ -1,6 +1,8 @@
package s3api
import (
"time"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
@@ -80,12 +82,74 @@ func (s3a *S3ApiServer) onCircuitBreakerConfigUpdate(dir, filename string, conte
func (s3a *S3ApiServer) onBucketMetadataChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
if dir == s3a.option.BucketsPath {
if newEntry != nil {
// Update bucket registry (existing functionality)
s3a.bucketRegistry.LoadBucketMetadata(newEntry)
glog.V(0).Infof("updated bucketMetadata %s/%s", dir, newEntry)
} else {
glog.V(0).Infof("updated bucketMetadata %s/%s", dir, newEntry.Name)
// Update bucket configuration cache with new entry
s3a.updateBucketConfigCacheFromEntry(newEntry)
} else if oldEntry != nil {
// Remove from bucket registry (existing functionality)
s3a.bucketRegistry.RemoveBucketMetadata(oldEntry)
glog.V(0).Infof("remove bucketMetadata %s/%s", dir, newEntry)
glog.V(0).Infof("remove bucketMetadata %s/%s", dir, oldEntry.Name)
// Remove from bucket configuration cache
s3a.invalidateBucketConfigCache(oldEntry.Name)
}
}
return nil
}
// updateBucketConfigCacheFromEntry updates the bucket config cache when a bucket entry changes
func (s3a *S3ApiServer) updateBucketConfigCacheFromEntry(entry *filer_pb.Entry) {
if s3a.bucketConfigCache == nil {
return
}
bucket := entry.Name
glog.V(2).Infof("updateBucketConfigCacheFromEntry: updating cache for bucket %s", bucket)
// Create new bucket config from the entry
config := &BucketConfig{
Name: bucket,
Entry: entry,
}
// Extract configuration from extended attributes
if entry.Extended != nil {
if versioning, exists := entry.Extended[s3_constants.ExtVersioningKey]; exists {
config.Versioning = string(versioning)
}
if ownership, exists := entry.Extended[s3_constants.ExtOwnershipKey]; exists {
config.Ownership = string(ownership)
}
if acl, exists := entry.Extended[s3_constants.ExtAmzAclKey]; exists {
config.ACL = acl
}
if owner, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
config.Owner = string(owner)
}
// Parse Object Lock configuration if present
if objectLockConfig, found := LoadObjectLockConfigurationFromExtended(entry); found {
config.ObjectLockConfig = objectLockConfig
glog.V(2).Infof("updateBucketConfigCacheFromEntry: cached Object Lock configuration for bucket %s", bucket)
}
}
// Update timestamp
config.LastModified = time.Now()
// Update cache
s3a.bucketConfigCache.Set(bucket, config)
glog.V(2).Infof("updateBucketConfigCacheFromEntry: updated bucket config cache for %s", bucket)
}
// invalidateBucketConfigCache removes a bucket from the configuration cache
func (s3a *S3ApiServer) invalidateBucketConfigCache(bucket string) {
if s3a.bucketConfigCache == nil {
return
}
s3a.bucketConfigCache.Remove(bucket)
glog.V(2).Infof("invalidateBucketConfigCache: removed bucket %s from cache", bucket)
}

View File

@@ -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
}

View File

@@ -20,7 +20,11 @@ const (
ExtRetentionUntilDateKey = "Seaweed-X-Amz-Retention-Until-Date"
ExtLegalHoldKey = "Seaweed-X-Amz-Legal-Hold"
ExtObjectLockEnabledKey = "Seaweed-X-Amz-Object-Lock-Enabled"
ExtObjectLockConfigKey = "Seaweed-X-Amz-Object-Lock-Config"
// Object Lock Bucket Configuration (individual components, not XML)
ExtObjectLockDefaultModeKey = "Lock-Default-Mode"
ExtObjectLockDefaultDaysKey = "Lock-Default-Days"
ExtObjectLockDefaultYearsKey = "Lock-Default-Years"
)
// Object Lock and Retention Constants

View File

@@ -17,24 +17,29 @@ import (
// BucketConfig represents cached bucket configuration
type BucketConfig struct {
Name string
Versioning string // "Enabled", "Suspended", or ""
Ownership string
ACL []byte
Owner string
CORS *cors.CORSConfiguration
LastModified time.Time
Entry *filer_pb.Entry
Name string
Versioning string // "Enabled", "Suspended", or ""
Ownership string
ACL []byte
Owner string
CORS *cors.CORSConfiguration
ObjectLockConfig *ObjectLockConfiguration // Cached parsed Object Lock configuration
LastModified time.Time
Entry *filer_pb.Entry
}
// BucketConfigCache provides caching for bucket configurations
// Cache entries are automatically updated/invalidated through metadata subscription events,
// so TTL serves as a safety fallback rather than the primary consistency mechanism
type BucketConfigCache struct {
cache map[string]*BucketConfig
mutex sync.RWMutex
ttl time.Duration
ttl time.Duration // Safety fallback TTL; real-time consistency maintained via events
}
// NewBucketConfigCache creates a new bucket configuration cache
// TTL can be set to a longer duration since cache consistency is maintained
// through real-time metadata subscription events rather than TTL expiration
func NewBucketConfigCache(ttl time.Duration) *BucketConfigCache {
return &BucketConfigCache{
cache: make(map[string]*BucketConfig),
@@ -52,7 +57,7 @@ func (bcc *BucketConfigCache) Get(bucket string) (*BucketConfig, bool) {
return nil, false
}
// Check if cache entry is expired
// Check if cache entry is expired (safety fallback; entries are normally updated via events)
if time.Since(config.LastModified) > bcc.ttl {
return nil, false
}
@@ -121,6 +126,11 @@ func (s3a *S3ApiServer) getBucketConfig(bucket string) (*BucketConfig, s3err.Err
if owner, exists := bucketEntry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
config.Owner = string(owner)
}
// Parse Object Lock configuration if present
if objectLockConfig, found := LoadObjectLockConfigurationFromExtended(bucketEntry); found {
config.ObjectLockConfig = objectLockConfig
glog.V(2).Infof("getBucketConfig: cached Object Lock configuration for bucket %s", bucket)
}
}
// Load CORS configuration from .s3metadata
@@ -173,6 +183,13 @@ func (s3a *S3ApiServer) updateBucketConfig(bucket string, updateFn func(*BucketC
if config.Owner != "" {
config.Entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(config.Owner)
}
// Update Object Lock configuration
if config.ObjectLockConfig != nil {
if err := StoreObjectLockConfigurationInExtended(config.Entry, config.ObjectLockConfig); err != nil {
glog.Errorf("updateBucketConfig: failed to store Object Lock configuration for bucket %s: %v", bucket, err)
return s3err.ErrInternalError
}
}
// Save to filer
err := s3a.updateEntry(s3a.option.BucketsPath, config.Entry)

View File

@@ -147,25 +147,13 @@ func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request)
// Enable versioning (required for Object Lock)
bucketConfig.Versioning = s3_constants.VersioningEnabled
// Enable Object Lock configuration
if bucketConfig.Entry.Extended == nil {
bucketConfig.Entry.Extended = make(map[string][]byte)
}
// Create basic Object Lock configuration (enabled without default retention)
// The ObjectLockConfiguration struct is defined below in this file.
objectLockConfig := &ObjectLockConfiguration{
ObjectLockEnabled: s3_constants.ObjectLockEnabled,
}
// Store the configuration as XML in extended attributes
configXML, err := xml.Marshal(objectLockConfig)
if err != nil {
return fmt.Errorf("failed to marshal Object Lock configuration to XML: %w", err)
}
bucketConfig.Entry.Extended[s3_constants.ExtObjectLockConfigKey] = configXML
bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(s3_constants.ObjectLockEnabled)
// Set the cached Object Lock configuration
bucketConfig.ObjectLockConfig = objectLockConfig
return nil
})

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -12,12 +12,11 @@ import (
"time"
"github.com/pquerna/cachecontrol/cacheobject"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
weed_server "github.com/seaweedfs/seaweedfs/weed/server"
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
)
@@ -32,6 +31,8 @@ var (
ErrObjectLockModeRequiresDate = errors.New("object lock mode requires retention until date")
ErrRetentionDateRequiresMode = errors.New("retention until date requires object lock mode")
ErrGovernanceBypassVersioningRequired = errors.New("governance bypass header can only be used on versioned buckets")
ErrInvalidObjectLockDuration = errors.New("object lock duration must be greater than 0 days")
ErrObjectLockDurationExceeded = errors.New("object lock duration exceeds maximum allowed days")
)
func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
@@ -374,28 +375,30 @@ func (s3a *S3ApiServer) updateLatestVersionInDirectory(bucket, object, versionId
}
// extractObjectLockMetadataFromRequest extracts object lock headers from PUT requests
// and stores them in the entry's Extended attributes
// and applies bucket default retention if no explicit retention is provided
func (s3a *S3ApiServer) extractObjectLockMetadataFromRequest(r *http.Request, entry *filer_pb.Entry) error {
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
// Extract object lock mode (GOVERNANCE or COMPLIANCE)
if mode := r.Header.Get(s3_constants.AmzObjectLockMode); mode != "" {
entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(mode)
glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing object lock mode: %s", mode)
// Extract explicit object lock mode (GOVERNANCE or COMPLIANCE)
explicitMode := r.Header.Get(s3_constants.AmzObjectLockMode)
if explicitMode != "" {
entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(explicitMode)
glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing explicit object lock mode: %s", explicitMode)
}
// Extract retention until date
if retainUntilDate := r.Header.Get(s3_constants.AmzObjectLockRetainUntilDate); retainUntilDate != "" {
// Extract explicit retention until date
explicitRetainUntilDate := r.Header.Get(s3_constants.AmzObjectLockRetainUntilDate)
if explicitRetainUntilDate != "" {
// Parse the ISO8601 date and convert to Unix timestamp for storage
parsedTime, err := time.Parse(time.RFC3339, retainUntilDate)
parsedTime, err := time.Parse(time.RFC3339, explicitRetainUntilDate)
if err != nil {
glog.Errorf("extractObjectLockMetadataFromRequest: failed to parse retention until date, expected format: %s, error: %v", time.RFC3339, err)
return ErrInvalidRetentionDateFormat
}
entry.Extended[s3_constants.ExtRetentionUntilDateKey] = []byte(strconv.FormatInt(parsedTime.Unix(), 10))
glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing retention until date (timestamp: %d)", parsedTime.Unix())
glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing explicit retention until date (timestamp: %d)", parsedTime.Unix())
}
// Extract legal hold status
@@ -410,6 +413,78 @@ func (s3a *S3ApiServer) extractObjectLockMetadataFromRequest(r *http.Request, en
}
}
// Apply bucket default retention if no explicit retention was provided
// This implements AWS S3 behavior where bucket default retention automatically applies to new objects
if explicitMode == "" && explicitRetainUntilDate == "" {
bucket, _ := s3_constants.GetBucketAndObject(r)
if err := s3a.applyBucketDefaultRetention(bucket, entry); err != nil {
glog.V(2).Infof("extractObjectLockMetadataFromRequest: skipping bucket default retention for %s: %v", bucket, err)
// Don't fail the upload if default retention can't be applied - this matches AWS behavior
}
}
return nil
}
// applyBucketDefaultRetention applies bucket default retention settings to a new object
// This implements AWS S3 behavior where bucket default retention automatically applies to new objects
// when no explicit retention headers are provided in the upload request
func (s3a *S3ApiServer) applyBucketDefaultRetention(bucket string, entry *filer_pb.Entry) error {
// Safety check - if bucket config cache is not available, skip default retention
if s3a.bucketConfigCache == nil {
return nil
}
// Get bucket configuration (getBucketConfig handles caching internally)
bucketConfig, errCode := s3a.getBucketConfig(bucket)
if errCode != s3err.ErrNone {
return fmt.Errorf("failed to get bucket config: %v", errCode)
}
// Check if bucket has cached Object Lock configuration
if bucketConfig.ObjectLockConfig == nil {
return nil // No Object Lock configuration
}
objectLockConfig := bucketConfig.ObjectLockConfig
// Check if there's a default retention rule
if objectLockConfig.Rule == nil || objectLockConfig.Rule.DefaultRetention == nil {
return nil // No default retention configured
}
defaultRetention := objectLockConfig.Rule.DefaultRetention
// Validate default retention has required fields
if defaultRetention.Mode == "" {
return fmt.Errorf("default retention missing mode")
}
if defaultRetention.Days == 0 && defaultRetention.Years == 0 {
return fmt.Errorf("default retention missing period")
}
// Calculate retention until date based on default retention period
var retainUntilDate time.Time
now := time.Now()
if defaultRetention.Days > 0 {
retainUntilDate = now.AddDate(0, 0, defaultRetention.Days)
} else if defaultRetention.Years > 0 {
retainUntilDate = now.AddDate(defaultRetention.Years, 0, 0)
}
// Apply default retention to the object
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(defaultRetention.Mode)
entry.Extended[s3_constants.ExtRetentionUntilDateKey] = []byte(strconv.FormatInt(retainUntilDate.Unix(), 10))
glog.V(2).Infof("applyBucketDefaultRetention: applied default retention %s until %s for bucket %s",
defaultRetention.Mode, retainUntilDate.Format(time.RFC3339), bucket)
return nil
}
@@ -493,6 +568,10 @@ func mapValidationErrorToS3Error(err error) s3err.ErrorCode {
return s3err.ErrInvalidRequest
case errors.Is(err, ErrGovernanceBypassVersioningRequired):
return s3err.ErrInvalidRequest
case errors.Is(err, ErrInvalidObjectLockDuration):
return s3err.ErrInvalidRequest
case errors.Is(err, ErrObjectLockDurationExceeded):
return s3err.ErrInvalidRequest
default:
return s3err.ErrInvalidRequest
}

View File

@@ -132,225 +132,3 @@ func (s3a *S3ApiServer) GetObjectRetentionHandler(w http.ResponseWriter, r *http
glog.V(3).Infof("GetObjectRetentionHandler: successfully retrieved retention for %s/%s", bucket, object)
}
// 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)
}
// 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 {
if bucketConfig.Entry.Extended == nil {
bucketConfig.Entry.Extended = make(map[string][]byte)
}
// Store the configuration as JSON in extended attributes
configXML, err := xml.Marshal(config)
if err != nil {
return err
}
bucketConfig.Entry.Extended[s3_constants.ExtObjectLockConfigKey] = configXML
if config.ObjectLockEnabled != "" {
bucketConfig.Entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(config.ObjectLockEnabled)
}
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
}
// Check if object lock configuration exists
if bucketConfig.Entry.Extended == nil {
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchObjectLockConfiguration)
return
}
configXML, exists := bucketConfig.Entry.Extended[s3_constants.ExtObjectLockConfigKey]
if !exists {
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)
}

View File

@@ -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")
})
}

View File

@@ -88,7 +88,7 @@ func NewS3ApiServerWithStore(router *mux.Router, option *S3ApiServerOption, expl
filerGuard: security.NewGuard([]string{}, signingKey, expiresAfterSec, readSigningKey, readExpiresAfterSec),
cb: NewCircuitBreaker(option),
credentialManager: iam.credentialManager,
bucketConfigCache: NewBucketConfigCache(5 * time.Minute),
bucketConfigCache: NewBucketConfigCache(60 * time.Minute), // Increased TTL since cache is now event-driven
}
if option.Config != "" {
@@ -286,8 +286,8 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
bucket.Methods(http.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketVersioningHandler, ACTION_WRITE)), "PUT")).Queries("versioning", "")
// GetObjectLockConfiguration / PutObjectLockConfiguration (bucket-level operations)
bucket.Methods(http.MethodGet).Path("/").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectLockConfigurationHandler, ACTION_READ)), "GET")).Queries("object-lock", "")
bucket.Methods(http.MethodPut).Path("/").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLockConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("object-lock", "")
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectLockConfigurationHandler, ACTION_READ)), "GET")).Queries("object-lock", "")
bucket.Methods(http.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLockConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("object-lock", "")
// GetBucketTagging
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketTaggingHandler, ACTION_TAGGING)), "GET")).Queries("tagging", "")