diff --git a/weed/shell/command_s3_bucket_create.go b/weed/shell/command_s3_bucket_create.go index ee6d3ec6a..5fcc7e7fb 100644 --- a/weed/shell/command_s3_bucket_create.go +++ b/weed/shell/command_s3_bucket_create.go @@ -31,6 +31,7 @@ func (c *commandS3BucketCreate) Help() string { Example: s3.bucket.create -name s3.bucket.create -name -owner + s3.bucket.create -name -withLock The -owner flag sets the bucket owner identity. This is important when using S3 IAM authentication, as non-admin users can only access buckets they own. @@ -39,6 +40,10 @@ func (c *commandS3BucketCreate) Help() string { The -owner value should match the identity name configured in your S3 IAM system (the "name" field in s3.json identities configuration). + + The -withLock flag enables S3 Object Lock on the bucket. This provides WORM + (Write Once Read Many) protection for objects. Once enabled, Object Lock + cannot be disabled. Versioning is automatically enabled when using this flag. ` } @@ -51,6 +56,7 @@ func (c *commandS3BucketCreate) Do(args []string, commandEnv *CommandEnv, writer bucketCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) bucketName := bucketCommand.String("name", "", "bucket name") bucketOwner := bucketCommand.String("owner", "", "bucket owner identity name (for S3 IAM authentication)") + withLock := bucketCommand.Bool("withLock", false, "enable Object Lock on the bucket (requires and enables versioning)") if err = bucketCommand.Parse(args); err != nil { return nil } @@ -95,6 +101,17 @@ func (c *commandS3BucketCreate) Do(args []string, commandEnv *CommandEnv, writer entry.Extended[s3_constants.AmzIdentityId] = []byte(owner) } + // Enable Object Lock if specified + if *withLock { + if entry.Extended == nil { + entry.Extended = make(map[string][]byte) + } + // Enable versioning (required for Object Lock) + entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled) + // Enable Object Lock + entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(s3_constants.ObjectLockEnabled) + } + if _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ Directory: filerBucketsPath, Entry: entry, @@ -106,6 +123,10 @@ func (c *commandS3BucketCreate) Do(args []string, commandEnv *CommandEnv, writer if owner != "" { fmt.Fprintln(writer, "bucket owner:", owner) } + if *withLock { + fmt.Fprintln(writer, "Object Lock: enabled") + fmt.Fprintln(writer, "Versioning: enabled") + } return nil diff --git a/weed/shell/command_s3_bucket_lock.go b/weed/shell/command_s3_bucket_lock.go new file mode 100644 index 000000000..9cbeda6e1 --- /dev/null +++ b/weed/shell/command_s3_bucket_lock.go @@ -0,0 +1,152 @@ +package shell + +import ( + "context" + "flag" + "fmt" + "io" + + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/util" +) + +func init() { + Commands = append(Commands, &commandS3BucketLock{}) +} + +type commandS3BucketLock struct { +} + +func (c *commandS3BucketLock) Name() string { + return "s3.bucket.lock" +} + +func (c *commandS3BucketLock) Help() string { + return `view or enable Object Lock for an S3 bucket + + Example: + # View the current Object Lock status of a bucket + s3.bucket.lock -name + + # Enable Object Lock on an existing bucket (irreversible) + s3.bucket.lock -name -enable + + Object Lock provides WORM (Write Once Read Many) protection for objects. + Once enabled, Object Lock cannot be disabled on a bucket. + + Requirements: + - Versioning will be automatically enabled if not already enabled + - Object Lock can only be enabled, never disabled (following AWS S3 behavior) + + After enabling Object Lock, you can: + - Set default retention policy on the bucket + - Set retention and legal hold on individual objects +` +} + +func (c *commandS3BucketLock) HasTag(CommandTag) bool { + return false +} + +func (c *commandS3BucketLock) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) { + + bucketCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + bucketName := bucketCommand.String("name", "", "bucket name") + enableLock := bucketCommand.Bool("enable", false, "enable Object Lock on the bucket (irreversible)") + if err = bucketCommand.Parse(args); err != nil { + return err + } + + if *bucketName == "" { + return fmt.Errorf("empty bucket name") + } + + err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { + + resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{}) + if err != nil { + return fmt.Errorf("get filer configuration: %w", err) + } + filerBucketsPath := resp.DirBuckets + + // Look up the bucket entry + lookupResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: filerBucketsPath, + Name: *bucketName, + }) + if err != nil { + return fmt.Errorf("lookup bucket %s: %w", *bucketName, err) + } + + entry := lookupResp.Entry + + // Check current Object Lock status + currentLockEnabled := false + currentVersioningEnabled := false + if entry.Extended != nil { + if lockStatus, ok := entry.Extended[s3_constants.ExtObjectLockEnabledKey]; ok { + currentLockEnabled = string(lockStatus) == s3_constants.ObjectLockEnabled + } + if versioningStatus, ok := entry.Extended[s3_constants.ExtVersioningKey]; ok { + currentVersioningEnabled = string(versioningStatus) == s3_constants.VersioningEnabled + } + } + + // If -enable is provided, enable Object Lock + if *enableLock { + if currentLockEnabled { + fmt.Fprintf(writer, "Object Lock is already enabled on bucket %s\n", *bucketName) + return nil + } + + if entry.Extended == nil { + entry.Extended = make(map[string][]byte) + } + + // Enable versioning if not already enabled (required for Object Lock) + if !currentVersioningEnabled { + entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled) + fmt.Fprintf(writer, "Enabling versioning on bucket %s (required for Object Lock)\n", *bucketName) + } + + // Enable Object Lock + entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(s3_constants.ObjectLockEnabled) + fmt.Fprintf(writer, "Enabling Object Lock on bucket %s\n", *bucketName) + + // Update the entry + if _, err := client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ + Directory: filerBucketsPath, + Entry: entry, + }); err != nil { + return fmt.Errorf("failed to update bucket: %w", err) + } + + fmt.Fprintf(writer, "Object Lock enabled successfully.\n") + fmt.Fprintf(writer, "WARNING: This action is irreversible. Object Lock cannot be disabled.\n") + return nil + } + + // Display current status (no flags provided) + fmt.Fprintf(writer, "Bucket: %s\n", *bucketName) + fmt.Fprintf(writer, "Path: %s\n", util.NewFullPath(filerBucketsPath, *bucketName)) + + if currentVersioningEnabled { + fmt.Fprintf(writer, "Versioning: Enabled\n") + } else { + fmt.Fprintf(writer, "Versioning: Disabled\n") + } + + if currentLockEnabled { + fmt.Fprintf(writer, "Object Lock: Enabled\n") + } else { + fmt.Fprintf(writer, "Object Lock: Disabled\n") + fmt.Fprintf(writer, "\nTo enable Object Lock, run:\n") + fmt.Fprintf(writer, " s3.bucket.lock -name %s -enable\n", *bucketName) + } + + return nil + }) + + return err +}