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:
@@ -437,8 +437,10 @@ func TestObjectLockConfiguration(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, types.ObjectLockEnabledEnabled, configResp.ObjectLockConfiguration.ObjectLockEnabled)
|
assert.Equal(t, types.ObjectLockEnabledEnabled, configResp.ObjectLockConfiguration.ObjectLockEnabled)
|
||||||
|
require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention, "DefaultRetention should not be nil")
|
||||||
|
require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days, "Days should not be nil")
|
||||||
assert.Equal(t, types.ObjectLockRetentionModeGovernance, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Mode)
|
assert.Equal(t, types.ObjectLockRetentionModeGovernance, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Mode)
|
||||||
assert.Equal(t, int32(30), configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days)
|
assert.Equal(t, int32(30), *configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRetentionWithVersions tests retention with specific object versions
|
// TestRetentionWithVersions tests retention with specific object versions
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -24,6 +23,8 @@ import (
|
|||||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminServer struct {
|
type AdminServer struct {
|
||||||
@@ -293,20 +294,11 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
|
|||||||
var objectLockDuration int32 = 0
|
var objectLockDuration int32 = 0
|
||||||
|
|
||||||
if resp.Entry.Extended != nil {
|
if resp.Entry.Extended != nil {
|
||||||
if versioningBytes, exists := resp.Entry.Extended["s3.versioning"]; exists {
|
// Use shared utility to extract versioning information
|
||||||
versioningEnabled = string(versioningBytes) == "Enabled"
|
versioningEnabled = extractVersioningFromEntry(resp.Entry)
|
||||||
}
|
|
||||||
if objectLockBytes, exists := resp.Entry.Extended["s3.objectlock"]; exists {
|
// Use shared utility to extract Object Lock information
|
||||||
objectLockEnabled = string(objectLockBytes) == "Enabled"
|
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(resp.Entry)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket := S3Bucket{
|
bucket := S3Bucket{
|
||||||
@@ -379,20 +371,11 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
|
|||||||
var objectLockDuration int32 = 0
|
var objectLockDuration int32 = 0
|
||||||
|
|
||||||
if bucketResp.Entry.Extended != nil {
|
if bucketResp.Entry.Extended != nil {
|
||||||
if versioningBytes, exists := bucketResp.Entry.Extended["s3.versioning"]; exists {
|
// Use shared utility to extract versioning information
|
||||||
versioningEnabled = string(versioningBytes) == "Enabled"
|
versioningEnabled = extractVersioningFromEntry(bucketResp.Entry)
|
||||||
}
|
|
||||||
if objectLockBytes, exists := bucketResp.Entry.Extended["s3.objectlock"]; exists {
|
// Use shared utility to extract Object Lock information
|
||||||
objectLockEnabled = string(objectLockBytes) == "Enabled"
|
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(bucketResp.Entry)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
details.Bucket.VersioningEnabled = versioningEnabled
|
details.Bucket.VersioningEnabled = versioningEnabled
|
||||||
@@ -1502,3 +1485,19 @@ func (s *AdminServer) Shutdown() {
|
|||||||
|
|
||||||
glog.V(1).Infof("Admin server shutdown complete")
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// S3 Bucket management data structures for templates
|
// S3 Bucket management data structures for templates
|
||||||
@@ -340,32 +341,43 @@ func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes
|
|||||||
TtlSec: 0,
|
TtlSec: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create extended attributes map for versioning and object lock
|
// Create extended attributes map for versioning
|
||||||
extended := make(map[string][]byte)
|
extended := make(map[string][]byte)
|
||||||
if versioningEnabled {
|
|
||||||
extended["s3.versioning"] = []byte("Enabled")
|
// Create bucket entry
|
||||||
} else {
|
bucketEntry := &filer_pb.Entry{
|
||||||
extended["s3.versioning"] = []byte("Suspended")
|
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 {
|
if objectLockEnabled {
|
||||||
extended["s3.objectlock"] = []byte("Enabled")
|
// Validate Object Lock parameters
|
||||||
extended["s3.objectlock.mode"] = []byte(objectLockMode)
|
if err := s3api.ValidateObjectLockParameters(objectLockEnabled, objectLockMode, objectLockDuration); err != nil {
|
||||||
extended["s3.objectlock.duration"] = []byte(fmt.Sprintf("%d", objectLockDuration))
|
return fmt.Errorf("invalid Object Lock parameters: %w", err)
|
||||||
} else {
|
}
|
||||||
extended["s3.objectlock"] = []byte("Disabled")
|
|
||||||
|
// 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
|
// Create bucket directory under /buckets
|
||||||
_, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
_, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
||||||
Directory: "/buckets",
|
Directory: "/buckets",
|
||||||
Entry: &filer_pb.Entry{
|
Entry: bucketEntry,
|
||||||
Name: bucketName,
|
|
||||||
IsDirectory: true,
|
|
||||||
Attributes: attributes,
|
|
||||||
Extended: extended,
|
|
||||||
Quota: quota,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create bucket directory: %w", err)
|
return fmt.Errorf("failed to create bucket directory: %w", err)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
"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 {
|
func (s3a *S3ApiServer) onBucketMetadataChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
|
||||||
if dir == s3a.option.BucketsPath {
|
if dir == s3a.option.BucketsPath {
|
||||||
if newEntry != nil {
|
if newEntry != nil {
|
||||||
|
// Update bucket registry (existing functionality)
|
||||||
s3a.bucketRegistry.LoadBucketMetadata(newEntry)
|
s3a.bucketRegistry.LoadBucketMetadata(newEntry)
|
||||||
glog.V(0).Infof("updated bucketMetadata %s/%s", dir, newEntry)
|
glog.V(0).Infof("updated bucketMetadata %s/%s", dir, newEntry.Name)
|
||||||
} else {
|
|
||||||
|
// Update bucket configuration cache with new entry
|
||||||
|
s3a.updateBucketConfigCacheFromEntry(newEntry)
|
||||||
|
} else if oldEntry != nil {
|
||||||
|
// Remove from bucket registry (existing functionality)
|
||||||
s3a.bucketRegistry.RemoveBucketMetadata(oldEntry)
|
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
|
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)
|
||||||
|
}
|
||||||
|
|||||||
232
weed/s3api/object_lock_utils.go
Normal file
232
weed/s3api/object_lock_utils.go
Normal 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
|
||||||
|
}
|
||||||
@@ -20,7 +20,11 @@ const (
|
|||||||
ExtRetentionUntilDateKey = "Seaweed-X-Amz-Retention-Until-Date"
|
ExtRetentionUntilDateKey = "Seaweed-X-Amz-Retention-Until-Date"
|
||||||
ExtLegalHoldKey = "Seaweed-X-Amz-Legal-Hold"
|
ExtLegalHoldKey = "Seaweed-X-Amz-Legal-Hold"
|
||||||
ExtObjectLockEnabledKey = "Seaweed-X-Amz-Object-Lock-Enabled"
|
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
|
// Object Lock and Retention Constants
|
||||||
|
|||||||
@@ -17,24 +17,29 @@ import (
|
|||||||
|
|
||||||
// BucketConfig represents cached bucket configuration
|
// BucketConfig represents cached bucket configuration
|
||||||
type BucketConfig struct {
|
type BucketConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Versioning string // "Enabled", "Suspended", or ""
|
Versioning string // "Enabled", "Suspended", or ""
|
||||||
Ownership string
|
Ownership string
|
||||||
ACL []byte
|
ACL []byte
|
||||||
Owner string
|
Owner string
|
||||||
CORS *cors.CORSConfiguration
|
CORS *cors.CORSConfiguration
|
||||||
LastModified time.Time
|
ObjectLockConfig *ObjectLockConfiguration // Cached parsed Object Lock configuration
|
||||||
Entry *filer_pb.Entry
|
LastModified time.Time
|
||||||
|
Entry *filer_pb.Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// BucketConfigCache provides caching for bucket configurations
|
// 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 {
|
type BucketConfigCache struct {
|
||||||
cache map[string]*BucketConfig
|
cache map[string]*BucketConfig
|
||||||
mutex sync.RWMutex
|
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
|
// 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 {
|
func NewBucketConfigCache(ttl time.Duration) *BucketConfigCache {
|
||||||
return &BucketConfigCache{
|
return &BucketConfigCache{
|
||||||
cache: make(map[string]*BucketConfig),
|
cache: make(map[string]*BucketConfig),
|
||||||
@@ -52,7 +57,7 @@ func (bcc *BucketConfigCache) Get(bucket string) (*BucketConfig, bool) {
|
|||||||
return nil, false
|
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 {
|
if time.Since(config.LastModified) > bcc.ttl {
|
||||||
return nil, false
|
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 {
|
if owner, exists := bucketEntry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
||||||
config.Owner = string(owner)
|
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
|
// Load CORS configuration from .s3metadata
|
||||||
@@ -173,6 +183,13 @@ func (s3a *S3ApiServer) updateBucketConfig(bucket string, updateFn func(*BucketC
|
|||||||
if config.Owner != "" {
|
if config.Owner != "" {
|
||||||
config.Entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(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
|
// Save to filer
|
||||||
err := s3a.updateEntry(s3a.option.BucketsPath, config.Entry)
|
err := s3a.updateEntry(s3a.option.BucketsPath, config.Entry)
|
||||||
|
|||||||
@@ -147,25 +147,13 @@ func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
// Enable versioning (required for Object Lock)
|
// Enable versioning (required for Object Lock)
|
||||||
bucketConfig.Versioning = s3_constants.VersioningEnabled
|
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)
|
// Create basic Object Lock configuration (enabled without default retention)
|
||||||
// The ObjectLockConfiguration struct is defined below in this file.
|
|
||||||
objectLockConfig := &ObjectLockConfiguration{
|
objectLockConfig := &ObjectLockConfiguration{
|
||||||
ObjectLockEnabled: s3_constants.ObjectLockEnabled,
|
ObjectLockEnabled: s3_constants.ObjectLockEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the configuration as XML in extended attributes
|
// Set the cached Object Lock configuration
|
||||||
configXML, err := xml.Marshal(objectLockConfig)
|
bucketConfig.ObjectLockConfig = 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)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
139
weed/s3api/s3api_bucket_handlers_object_lock_config.go
Normal file
139
weed/s3api/s3api_bucket_handlers_object_lock_config.go
Normal 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)
|
||||||
|
}
|
||||||
126
weed/s3api/s3api_object_handlers_legal_hold.go
Normal file
126
weed/s3api/s3api_object_handlers_legal_hold.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -12,12 +12,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pquerna/cachecontrol/cacheobject"
|
"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/s3_constants"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
"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"
|
weed_server "github.com/seaweedfs/seaweedfs/weed/server"
|
||||||
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
||||||
)
|
)
|
||||||
@@ -32,6 +31,8 @@ var (
|
|||||||
ErrObjectLockModeRequiresDate = errors.New("object lock mode requires retention until date")
|
ErrObjectLockModeRequiresDate = errors.New("object lock mode requires retention until date")
|
||||||
ErrRetentionDateRequiresMode = errors.New("retention until date requires object lock mode")
|
ErrRetentionDateRequiresMode = errors.New("retention until date requires object lock mode")
|
||||||
ErrGovernanceBypassVersioningRequired = errors.New("governance bypass header can only be used on versioned buckets")
|
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) {
|
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
|
// 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 {
|
func (s3a *S3ApiServer) extractObjectLockMetadataFromRequest(r *http.Request, entry *filer_pb.Entry) error {
|
||||||
if entry.Extended == nil {
|
if entry.Extended == nil {
|
||||||
entry.Extended = make(map[string][]byte)
|
entry.Extended = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract object lock mode (GOVERNANCE or COMPLIANCE)
|
// Extract explicit object lock mode (GOVERNANCE or COMPLIANCE)
|
||||||
if mode := r.Header.Get(s3_constants.AmzObjectLockMode); mode != "" {
|
explicitMode := r.Header.Get(s3_constants.AmzObjectLockMode)
|
||||||
entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(mode)
|
if explicitMode != "" {
|
||||||
glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing object lock mode: %s", mode)
|
entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(explicitMode)
|
||||||
|
glog.V(2).Infof("extractObjectLockMetadataFromRequest: storing explicit object lock mode: %s", explicitMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract retention until date
|
// Extract explicit retention until date
|
||||||
if retainUntilDate := r.Header.Get(s3_constants.AmzObjectLockRetainUntilDate); retainUntilDate != "" {
|
explicitRetainUntilDate := r.Header.Get(s3_constants.AmzObjectLockRetainUntilDate)
|
||||||
|
if explicitRetainUntilDate != "" {
|
||||||
// Parse the ISO8601 date and convert to Unix timestamp for storage
|
// 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 {
|
if err != nil {
|
||||||
glog.Errorf("extractObjectLockMetadataFromRequest: failed to parse retention until date, expected format: %s, error: %v", time.RFC3339, err)
|
glog.Errorf("extractObjectLockMetadataFromRequest: failed to parse retention until date, expected format: %s, error: %v", time.RFC3339, err)
|
||||||
return ErrInvalidRetentionDateFormat
|
return ErrInvalidRetentionDateFormat
|
||||||
}
|
}
|
||||||
entry.Extended[s3_constants.ExtRetentionUntilDateKey] = []byte(strconv.FormatInt(parsedTime.Unix(), 10))
|
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
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +568,10 @@ func mapValidationErrorToS3Error(err error) s3err.ErrorCode {
|
|||||||
return s3err.ErrInvalidRequest
|
return s3err.ErrInvalidRequest
|
||||||
case errors.Is(err, ErrGovernanceBypassVersioningRequired):
|
case errors.Is(err, ErrGovernanceBypassVersioningRequired):
|
||||||
return s3err.ErrInvalidRequest
|
return s3err.ErrInvalidRequest
|
||||||
|
case errors.Is(err, ErrInvalidObjectLockDuration):
|
||||||
|
return s3err.ErrInvalidRequest
|
||||||
|
case errors.Is(err, ErrObjectLockDurationExceeded):
|
||||||
|
return s3err.ErrInvalidRequest
|
||||||
default:
|
default:
|
||||||
return s3err.ErrInvalidRequest
|
return s3err.ErrInvalidRequest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
90
weed/s3api/s3api_object_lock_fix_test.go
Normal file
90
weed/s3api/s3api_object_lock_fix_test.go
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ func NewS3ApiServerWithStore(router *mux.Router, option *S3ApiServerOption, expl
|
|||||||
filerGuard: security.NewGuard([]string{}, signingKey, expiresAfterSec, readSigningKey, readExpiresAfterSec),
|
filerGuard: security.NewGuard([]string{}, signingKey, expiresAfterSec, readSigningKey, readExpiresAfterSec),
|
||||||
cb: NewCircuitBreaker(option),
|
cb: NewCircuitBreaker(option),
|
||||||
credentialManager: iam.credentialManager,
|
credentialManager: iam.credentialManager,
|
||||||
bucketConfigCache: NewBucketConfigCache(5 * time.Minute),
|
bucketConfigCache: NewBucketConfigCache(60 * time.Minute), // Increased TTL since cache is now event-driven
|
||||||
}
|
}
|
||||||
|
|
||||||
if option.Config != "" {
|
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", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketVersioningHandler, ACTION_WRITE)), "PUT")).Queries("versioning", "")
|
||||||
|
|
||||||
// GetObjectLockConfiguration / PutObjectLockConfiguration (bucket-level operations)
|
// 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.MethodGet).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.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLockConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("object-lock", "")
|
||||||
|
|
||||||
// GetBucketTagging
|
// GetBucketTagging
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketTaggingHandler, ACTION_TAGGING)), "GET")).Queries("tagging", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketTaggingHandler, ACTION_TAGGING)), "GET")).Queries("tagging", "")
|
||||||
|
|||||||
Reference in New Issue
Block a user