Files
seaweedFS/weed/s3api/object_lock_utils.go
Chris Lu 995dfc4d5d chore: remove ~50k lines of unreachable dead code (#8913)
* chore: remove unreachable dead code across the codebase

Remove ~50,000 lines of unreachable code identified by static analysis.

Major removals:
- weed/filer/redis_lua: entire unused Redis Lua filer store implementation
- weed/wdclient/net2, resource_pool: unused connection/resource pool packages
- weed/plugin/worker/lifecycle: unused lifecycle plugin worker
- weed/s3api: unused S3 policy templates, presigned URL IAM, streaming copy,
  multipart IAM, key rotation, and various SSE helper functions
- weed/mq/kafka: unused partition mapping, compression, schema, and protocol functions
- weed/mq/offset: unused SQL storage and migration code
- weed/worker: unused registry, task, and monitoring functions
- weed/query: unused SQL engine, parquet scanner, and type functions
- weed/shell: unused EC proportional rebalance functions
- weed/storage/erasure_coding/distribution: unused distribution analysis functions
- Individual unreachable functions removed from 150+ files across admin,
  credential, filer, iam, kms, mount, mq, operation, pb, s3api, server,
  shell, storage, topology, and util packages

* fix(s3): reset shared memory store in IAM test to prevent flaky failure

TestLoadIAMManagerFromConfig_EmptyConfigWithFallbackKey was flaky because
the MemoryStore credential backend is a singleton registered via init().
Earlier tests that create anonymous identities pollute the shared store,
causing LookupAnonymous() to unexpectedly return true.

Fix by calling Reset() on the memory store before the test runs.

* style: run gofmt on changed files

* fix: restore KMS functions used by integration tests

* fix(plugin): prevent panic on send to closed worker session channel

The Plugin.sendToWorker method could panic with "send on closed channel"
when a worker disconnected while a message was being sent. The race was
between streamSession.close() closing the outgoing channel and sendToWorker
writing to it concurrently.

Add a done channel to streamSession that is closed before the outgoing
channel, and check it in sendToWorker's select to safely detect closed
sessions without panicking.
2026-04-03 16:04:27 -07:00

368 lines
12 KiB
Go

package s3api
import (
"context"
"strconv"
"time"
"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_objectlock"
)
// 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 {
// Don't set the header when versioning is not enabled
delete(entry.Extended, s3_constants.ExtVersioningKey)
}
return nil
}
// GetVersioningStatus returns the versioning status as a string: "", "Enabled", or "Suspended"
// Empty string means versioning was never enabled
func GetVersioningStatus(entry *filer_pb.Entry) string {
if entry == nil || entry.Extended == nil {
return "" // Never enabled
}
if versioningBytes, exists := entry.Extended[s3_constants.ExtVersioningKey]; exists {
return string(versioningBytes) // "Enabled" or "Suspended"
}
return "" // Never enabled
}
// 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,
DaysSet: days > 0,
YearsSet: years > 0,
},
}
}
return 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.DaysSet && defaultRetention.Days > 0 {
entry.Extended[s3_constants.ExtObjectLockDefaultDaysKey] = []byte(strconv.Itoa(defaultRetention.Days))
}
// Store years
if defaultRetention.YearsSet && 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,
DaysSet: days > 0,
YearsSet: years > 0,
},
}
}
}
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 := 0
if defaultRetention.DaysSet {
days = defaultRetention.Days
}
if defaultRetention.YearsSet && 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
}
// ====================================================================
// OBJECT LOCK VALIDATION FUNCTIONS
// ====================================================================
// These validation functions provide comprehensive validation for
// all Object Lock related configurations and requests.
// ValidateRetention validates retention configuration for object-level retention
func ValidateRetention(retention *ObjectRetention) error {
// Check if mode is specified
if retention.Mode == "" {
return ErrRetentionMissingMode
}
// Check if retain until date is specified
if retention.RetainUntilDate == nil {
return ErrRetentionMissingRetainUntilDate
}
// Check if mode is valid
if retention.Mode != s3_constants.RetentionModeGovernance && retention.Mode != s3_constants.RetentionModeCompliance {
return ErrInvalidRetentionModeValue
}
// Check if retain until date is in the future
if retention.RetainUntilDate.Before(time.Now()) {
return ErrRetentionDateMustBeFuture
}
return nil
}
// ValidateLegalHold validates legal hold configuration
func ValidateLegalHold(legalHold *ObjectLegalHold) error {
// Check if status is valid
if legalHold.Status != s3_constants.LegalHoldOn && legalHold.Status != s3_constants.LegalHoldOff {
return ErrInvalidLegalHoldStatus
}
return nil
}
// ValidateObjectLockConfiguration validates object lock configuration at bucket level
func ValidateObjectLockConfiguration(config *ObjectLockConfiguration) error {
// ObjectLockEnabled is required for bucket-level configuration
if config.ObjectLockEnabled == "" {
return ErrObjectLockConfigurationMissingEnabled
}
// Validate ObjectLockEnabled value
if config.ObjectLockEnabled != s3_constants.ObjectLockEnabled {
// ObjectLockEnabled can only be 'Enabled', any other value (including 'Disabled') is malformed XML
return ErrInvalidObjectLockEnabledValue
}
// Validate Rule if present
if config.Rule != nil {
if config.Rule.DefaultRetention == nil {
return ErrRuleMissingDefaultRetention
}
return validateDefaultRetention(config.Rule.DefaultRetention)
}
return nil
}
// validateDefaultRetention validates default retention configuration for bucket-level settings
func validateDefaultRetention(retention *DefaultRetention) error {
glog.V(2).Infof("validateDefaultRetention: Mode=%s, Days=%d (set=%v), Years=%d (set=%v)",
retention.Mode, retention.Days, retention.DaysSet, retention.Years, retention.YearsSet)
// Mode is required
if retention.Mode == "" {
return ErrDefaultRetentionMissingMode
}
// Mode must be valid
if retention.Mode != s3_constants.RetentionModeGovernance && retention.Mode != s3_constants.RetentionModeCompliance {
return ErrInvalidDefaultRetentionMode
}
// Check for invalid Years value (negative values are always invalid)
if retention.YearsSet && retention.Years < 0 {
return ErrInvalidRetentionPeriod
}
// Check for invalid Days value (negative values are invalid)
if retention.DaysSet && retention.Days < 0 {
return ErrInvalidRetentionPeriod
}
// Check for invalid Days value (zero is invalid when explicitly provided)
if retention.DaysSet && retention.Days == 0 {
return ErrInvalidRetentionPeriod
}
// Check for neither Days nor Years being specified
if !retention.DaysSet && !retention.YearsSet {
return ErrDefaultRetentionMissingPeriod
}
// Check for both Days and Years being specified
if retention.DaysSet && retention.YearsSet {
return ErrDefaultRetentionBothDaysAndYears
}
// Validate Days if specified
if retention.DaysSet && retention.Days > 0 {
if retention.Days > MaxRetentionDays {
return ErrDefaultRetentionDaysOutOfRange
}
}
// Validate Years if specified
if retention.YearsSet && retention.Years > 0 {
if retention.Years > MaxRetentionYears {
return ErrDefaultRetentionYearsOutOfRange
}
}
return nil
}
// HasObjectsWithActiveLocks checks if any objects in the bucket have active retention or legal hold
// Delegates to s3_objectlock.HasObjectsWithActiveLocks
func HasObjectsWithActiveLocks(ctx context.Context, client filer_pb.SeaweedFilerClient, bucketPath string) (bool, error) {
return s3_objectlock.HasObjectsWithActiveLocks(ctx, client, bucketPath)
}
// CheckBucketForLockedObjects is a unified function that checks if a bucket has Object Lock enabled
// and if so, scans for objects with active locks.
// Delegates to s3_objectlock.CheckBucketForLockedObjects
func CheckBucketForLockedObjects(ctx context.Context, client filer_pb.SeaweedFilerClient, bucketsPath, bucketName string) error {
return s3_objectlock.CheckBucketForLockedObjects(ctx, client, bucketsPath, bucketName)
}