From fbaba0b9444be53ff1ed6b882a6099b371134a1f Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Mon, 15 Apr 2024 07:01:53 -0400 Subject: [PATCH 1/3] feat: Added object WORM protection by object-lock feature from AWS with the following actions support: PutObjectLockConfiguration, GetObjectLockConfiguration, PutObjectRetention, GetObjectRetention, PutObjectLegalHold, GetObjectLegalHold --- auth/bucket_policy.go | 1 - auth/bucket_policy_actions.go | 152 +++++----- auth/object_lock.go | 150 ++++++++++ backend/backend.go | 28 ++ backend/posix/posix.go | 257 ++++++++++++++++ s3api/controllers/backend_moq_test.go | 409 +++++++++++++++++++++++--- s3api/controllers/base.go | 258 ++++++++++++++++ s3api/controllers/base_test.go | 143 ++++++++- s3api/middlewares/acl-parser.go | 3 +- s3api/utils/utils.go | 9 + s3err/s3err.go | 30 ++ 11 files changed, 1328 insertions(+), 112 deletions(-) create mode 100644 auth/object_lock.go diff --git a/auth/bucket_policy.go b/auth/bucket_policy.go index 116f62a2..e703853d 100644 --- a/auth/bucket_policy.go +++ b/auth/bucket_policy.go @@ -131,7 +131,6 @@ func verifyBucketPolicy(policy []byte, access, bucket, object string, action Act resource += "/" + object } - fmt.Println(access, action, resource) if !bucketPolicy.isAllowed(access, action, resource) { return s3err.GetAPIError(s3err.ErrAccessDenied) } diff --git a/auth/bucket_policy_actions.go b/auth/bucket_policy_actions.go index 028a7b9c..ea7988df 100644 --- a/auth/bucket_policy_actions.go +++ b/auth/bucket_policy_actions.go @@ -23,79 +23,97 @@ import ( type Action string const ( - GetBucketAclAction Action = "s3:GetBucketAcl" - CreateBucketAction Action = "s3:CreateBucket" - PutBucketAclAction Action = "s3:PutBucketAcl" - DeleteBucketAction Action = "s3:DeleteBucket" - PutBucketVersioningAction Action = "s3:PutBucketVersioning" - GetBucketVersioningAction Action = "s3:GetBucketVersioning" - PutBucketPolicyAction Action = "s3:PutBucketPolicy" - GetBucketPolicyAction Action = "s3:GetBucketPolicy" - DeleteBucketPolicyAction Action = "s3:DeleteBucketPolicy" - AbortMultipartUploadAction Action = "s3:AbortMultipartUpload" - ListMultipartUploadPartsAction Action = "s3:ListMultipartUploadParts" - ListBucketMultipartUploadsAction Action = "s3:ListBucketMultipartUploads" - PutObjectAction Action = "s3:PutObject" - GetObjectAction Action = "s3:GetObject" - DeleteObjectAction Action = "s3:DeleteObject" - GetObjectAclAction Action = "s3:GetObjectAcl" - GetObjectAttributesAction Action = "s3:GetObjectAttributes" - PutObjectAclAction Action = "s3:PutObjectAcl" - RestoreObjectAction Action = "s3:RestoreObject" - GetBucketTaggingAction Action = "s3:GetBucketTagging" - PutBucketTaggingAction Action = "s3:PutBucketTagging" - GetObjectTaggingAction Action = "s3:GetObjectTagging" - PutObjectTaggingAction Action = "s3:PutObjectTagging" - DeleteObjectTaggingAction Action = "s3:DeleteObjectTagging" - ListBucketVersionsAction Action = "s3:ListBucketVersions" - ListBucketAction Action = "s3:ListBucket" - AllActions Action = "s3:*" + GetBucketAclAction Action = "s3:GetBucketAcl" + CreateBucketAction Action = "s3:CreateBucket" + PutBucketAclAction Action = "s3:PutBucketAcl" + DeleteBucketAction Action = "s3:DeleteBucket" + PutBucketVersioningAction Action = "s3:PutBucketVersioning" + GetBucketVersioningAction Action = "s3:GetBucketVersioning" + PutBucketPolicyAction Action = "s3:PutBucketPolicy" + GetBucketPolicyAction Action = "s3:GetBucketPolicy" + DeleteBucketPolicyAction Action = "s3:DeleteBucketPolicy" + AbortMultipartUploadAction Action = "s3:AbortMultipartUpload" + ListMultipartUploadPartsAction Action = "s3:ListMultipartUploadParts" + ListBucketMultipartUploadsAction Action = "s3:ListBucketMultipartUploads" + PutObjectAction Action = "s3:PutObject" + GetObjectAction Action = "s3:GetObject" + DeleteObjectAction Action = "s3:DeleteObject" + GetObjectAclAction Action = "s3:GetObjectAcl" + GetObjectAttributesAction Action = "s3:GetObjectAttributes" + PutObjectAclAction Action = "s3:PutObjectAcl" + RestoreObjectAction Action = "s3:RestoreObject" + GetBucketTaggingAction Action = "s3:GetBucketTagging" + PutBucketTaggingAction Action = "s3:PutBucketTagging" + GetObjectTaggingAction Action = "s3:GetObjectTagging" + PutObjectTaggingAction Action = "s3:PutObjectTagging" + DeleteObjectTaggingAction Action = "s3:DeleteObjectTagging" + ListBucketVersionsAction Action = "s3:ListBucketVersions" + ListBucketAction Action = "s3:ListBucket" + GetBucketObjectLockConfigurationAction Action = "s3:GetBucketObjectLockConfiguration" + PutBucketObjectLockConfigurationAction Action = "s3:PutBucketObjectLockConfiguration" + GetObjectLegalHoldAction Action = "s3:GetObjectLegalHold" + PutObjectLegalHoldAction Action = "s3:PutObjectLegalHold" + GetObjectRetentionAction Action = "s3:GetObjectRetention" + PutObjectRetentionAction Action = "s3:PutObjectRetention" + BypassGovernanceRetentionAction Action = "s3:BypassGovernanceRetention" + AllActions Action = "s3:*" ) var supportedActionList = map[Action]struct{}{ - GetBucketAclAction: {}, - CreateBucketAction: {}, - PutBucketAclAction: {}, - DeleteBucketAction: {}, - PutBucketVersioningAction: {}, - GetBucketVersioningAction: {}, - PutBucketPolicyAction: {}, - GetBucketPolicyAction: {}, - DeleteBucketPolicyAction: {}, - AbortMultipartUploadAction: {}, - ListMultipartUploadPartsAction: {}, - ListBucketMultipartUploadsAction: {}, - PutObjectAction: {}, - GetObjectAction: {}, - DeleteObjectAction: {}, - GetObjectAclAction: {}, - GetObjectAttributesAction: {}, - PutObjectAclAction: {}, - RestoreObjectAction: {}, - GetBucketTaggingAction: {}, - PutBucketTaggingAction: {}, - GetObjectTaggingAction: {}, - PutObjectTaggingAction: {}, - DeleteObjectTaggingAction: {}, - ListBucketVersionsAction: {}, - ListBucketAction: {}, - AllActions: {}, + GetBucketAclAction: {}, + CreateBucketAction: {}, + PutBucketAclAction: {}, + DeleteBucketAction: {}, + PutBucketVersioningAction: {}, + GetBucketVersioningAction: {}, + PutBucketPolicyAction: {}, + GetBucketPolicyAction: {}, + DeleteBucketPolicyAction: {}, + AbortMultipartUploadAction: {}, + ListMultipartUploadPartsAction: {}, + ListBucketMultipartUploadsAction: {}, + PutObjectAction: {}, + GetObjectAction: {}, + DeleteObjectAction: {}, + GetObjectAclAction: {}, + GetObjectAttributesAction: {}, + PutObjectAclAction: {}, + RestoreObjectAction: {}, + GetBucketTaggingAction: {}, + PutBucketTaggingAction: {}, + GetObjectTaggingAction: {}, + PutObjectTaggingAction: {}, + DeleteObjectTaggingAction: {}, + ListBucketVersionsAction: {}, + ListBucketAction: {}, + PutBucketObjectLockConfigurationAction: {}, + GetObjectLegalHoldAction: {}, + PutObjectLegalHoldAction: {}, + GetObjectRetentionAction: {}, + PutObjectRetentionAction: {}, + BypassGovernanceRetentionAction: {}, + AllActions: {}, } var supportedObjectActionList = map[Action]struct{}{ - AbortMultipartUploadAction: {}, - ListMultipartUploadPartsAction: {}, - PutObjectAction: {}, - GetObjectAction: {}, - DeleteObjectAction: {}, - GetObjectAclAction: {}, - GetObjectAttributesAction: {}, - PutObjectAclAction: {}, - RestoreObjectAction: {}, - GetObjectTaggingAction: {}, - PutObjectTaggingAction: {}, - DeleteObjectTaggingAction: {}, - AllActions: {}, + AbortMultipartUploadAction: {}, + ListMultipartUploadPartsAction: {}, + PutObjectAction: {}, + GetObjectAction: {}, + DeleteObjectAction: {}, + GetObjectAclAction: {}, + GetObjectAttributesAction: {}, + PutObjectAclAction: {}, + RestoreObjectAction: {}, + GetObjectTaggingAction: {}, + PutObjectTaggingAction: {}, + DeleteObjectTaggingAction: {}, + GetObjectLegalHoldAction: {}, + PutObjectLegalHoldAction: {}, + GetObjectRetentionAction: {}, + PutObjectRetentionAction: {}, + BypassGovernanceRetentionAction: {}, + AllActions: {}, } // Validates Action: it should either wildcard match with supported actions list or be in it diff --git a/auth/object_lock.go b/auth/object_lock.go new file mode 100644 index 00000000..6882790c --- /dev/null +++ b/auth/object_lock.go @@ -0,0 +1,150 @@ +// Copyright 2023 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package auth + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/s3err" +) + +type BucketLockConfig struct { + Enabled bool + DefaultRetention *types.DefaultRetention + CreatedAt *time.Time +} + +type ObjectLockConfig struct { + LegalHoldEnabled bool + Retention *types.ObjectLockRetention +} + +func ParseBucketLockConfigurationOutput(input []byte) (*types.ObjectLockConfiguration, error) { + var config BucketLockConfig + if err := json.Unmarshal(input, &config); err != nil { + return nil, fmt.Errorf("parse object lock config: %w", err) + } + + result := &types.ObjectLockConfiguration{ + Rule: &types.ObjectLockRule{ + DefaultRetention: config.DefaultRetention, + }, + } + + if config.Enabled { + result.ObjectLockEnabled = types.ObjectLockEnabledEnabled + } + + return result, nil +} + +func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []string, isAdminOrRoot bool, be backend.Backend) error { + data, err := be.GetObjectLockConfiguration(ctx, bucket) + if err != nil { + if errors.Is(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)) { + return nil + } + + return err + } + + var bucketLockConfig BucketLockConfig + if err := json.Unmarshal(data, &bucketLockConfig); err != nil { + return fmt.Errorf("parse object lock config: %w", err) + } + + if !bucketLockConfig.Enabled { + return nil + } + + for _, obj := range objects { + retention, err := be.GetObjectRetention(ctx, bucket, obj, "") + if err != nil { + if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { + continue + } + if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) { + continue + } + + return err + } + + if retention.Mode != "" && retention.RetainUntilDate != nil { + if retention.RetainUntilDate.After(time.Now()) { + switch retention.Mode { + case types.ObjectLockRetentionModeGovernance: + if !isAdminOrRoot { + policy, err := be.GetBucketPolicy(ctx, bucket) + if err != nil { + return err + } + err = verifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) + if err != nil { + return s3err.GetAPIError(s3err.ErrObjectLocked) + } + } + case types.ObjectLockRetentionModeCompliance: + return s3err.GetAPIError(s3err.ErrObjectLocked) + } + } + } + + legalHold, err := be.GetObjectLegalHold(ctx, bucket, obj, "") + if err != nil { + return err + } + + if legalHold.Status == types.ObjectLockLegalHoldStatusOn && !isAdminOrRoot { + return s3err.GetAPIError(s3err.ErrObjectLocked) + } + } + + if bucketLockConfig.DefaultRetention != nil && bucketLockConfig.CreatedAt != nil { + expirationDate := *bucketLockConfig.CreatedAt + if bucketLockConfig.DefaultRetention.Days != nil { + expirationDate = expirationDate.AddDate(0, 0, int(*bucketLockConfig.DefaultRetention.Days)) + } + if bucketLockConfig.DefaultRetention.Years != nil { + expirationDate = expirationDate.AddDate(int(*bucketLockConfig.DefaultRetention.Years), 0, 0) + } + + if expirationDate.After(time.Now()) { + switch bucketLockConfig.DefaultRetention.Mode { + case types.ObjectLockRetentionModeGovernance: + if !isAdminOrRoot { + policy, err := be.GetBucketPolicy(ctx, bucket) + if err != nil { + return err + } + err = verifyBucketPolicy(policy, userAccess, bucket, "", BypassGovernanceRetentionAction) + if err != nil { + return s3err.GetAPIError(s3err.ErrObjectLocked) + } + } + case types.ObjectLockRetentionModeCompliance: + return s3err.GetAPIError(s3err.ErrObjectLocked) + } + } + } + + return nil +} diff --git a/backend/backend.go b/backend/backend.go index b56e654a..a0a79d17 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -21,6 +21,7 @@ import ( "io" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3response" "github.com/versity/versitygw/s3select" @@ -81,6 +82,14 @@ type Backend interface { PutObjectTagging(_ context.Context, bucket, object string, tags map[string]string) error DeleteObjectTagging(_ context.Context, bucket, object string) error + // object lock operations + PutObjectLockConfiguration(context.Context, *s3.PutObjectLockConfigurationInput) error + GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) + PutObjectRetention(context.Context, *s3.PutObjectRetentionInput) error + GetObjectRetention(_ context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) + PutObjectLegalHold(context.Context, *s3.PutObjectLegalHoldInput) error + GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) + // non AWS actions ChangeBucketOwner(_ context.Context, bucket, newOwner string) error ListBucketsAndOwners(context.Context) ([]s3response.Bucket, error) @@ -229,6 +238,25 @@ func (BackendUnsupported) DeleteObjectTagging(_ context.Context, bucket, object return s3err.GetAPIError(s3err.ErrNotImplemented) } +func (BackendUnsupported) PutObjectLockConfiguration(context.Context, *s3.PutObjectLockConfigurationInput) error { + return s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) PutObjectRetention(context.Context, *s3.PutObjectRetentionInput) error { + return s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) GetObjectRetention(_ context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) PutObjectLegalHold(context.Context, *s3.PutObjectLegalHoldInput) error { + return s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) +} + func (BackendUnsupported) ChangeBucketOwner(_ context.Context, bucket, newOwner string) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 0cb69ad8..3fc6e320 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -30,10 +30,12 @@ import ( "strconv" "strings" "syscall" + "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/google/uuid" + "github.com/pkg/xattr" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/backend/meta" @@ -75,6 +77,8 @@ const ( aclkey = "acl" etagkey = "etag" policykey = "policy" + bucketLockKey = "bucket-lock" + objectLockKey = "object-lock" ) type PosixOpts struct { @@ -2011,6 +2015,259 @@ func (p *Posix) DeleteBucketPolicy(ctx context.Context, bucket string) error { return p.PutBucketPolicy(ctx, bucket, nil) } +func (p *Posix) PutObjectLockConfiguration(_ context.Context, input *s3.PutObjectLockConfigurationInput) error { + _, err := os.Stat(*input.Bucket) + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchBucket) + } + if err != nil { + return fmt.Errorf("stat bucket: %w", err) + } + + lockConfig := input.ObjectLockConfiguration + + config := auth.BucketLockConfig{ + Enabled: lockConfig.ObjectLockEnabled == types.ObjectLockEnabledEnabled, + } + + if lockConfig.Rule != nil && lockConfig.Rule.DefaultRetention != nil { + retentation := lockConfig.Rule.DefaultRetention + if retentation.Years != nil && retentation.Days != nil { + return s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + config.DefaultRetention = retentation + now := time.Now() + config.CreatedAt = &now + } + + configParsed, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("parse object lock config: %w", err) + } + + if err := xattr.Set(*input.Bucket, bucketLockKey, configParsed); err != nil { + return fmt.Errorf("set tags: %w", err) + } + + return nil +} + +func (p *Posix) GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) { + _, err := os.Stat(bucket) + if errors.Is(err, fs.ErrNotExist) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + } + if err != nil { + return nil, fmt.Errorf("stat bucket: %w", err) + } + + cfg, err := xattr.Get(bucket, bucketLockKey) + if errors.Is(err, meta.ErrNoSuchKey) { + return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + } + if err != nil { + return nil, fmt.Errorf("get object lock config: %w", err) + } + + return cfg, nil +} + +func (p *Posix) PutObjectLegalHold(_ context.Context, input *s3.PutObjectLegalHoldInput) error { + _, err := os.Stat(*input.Bucket) + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchBucket) + } + if err != nil { + return fmt.Errorf("stat bucket: %w", err) + } + + cfg, err := xattr.Get(*input.Bucket, bucketLockKey) + if errors.Is(err, meta.ErrNoSuchKey) { + return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) + } + if err != nil { + return fmt.Errorf("get object lock config: %w", err) + } + + var bucketLockConfig auth.BucketLockConfig + if err := json.Unmarshal(cfg, &bucketLockConfig); err != nil { + return fmt.Errorf("parse bucket lock config: %w", err) + } + + if !bucketLockConfig.Enabled { + return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) + } + + path := filepath.Join(*input.Bucket, *input.Key) + var config auth.ObjectLockConfig + + data, err := xattr.Get(path, objectLockKey) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if errors.Is(err, meta.ErrNoSuchKey) { + return fmt.Errorf("get object lock config: %w", err) + } + + config = auth.ObjectLockConfig{} + } else { + if err := json.Unmarshal(data, &config); err != nil { + return fmt.Errorf("parse object lock data %w", err) + } + } + + switch input.LegalHold.Status { + case types.ObjectLockLegalHoldStatusOff: + config.LegalHoldEnabled = false + case types.ObjectLockLegalHoldStatusOn: + config.LegalHoldEnabled = true + default: + return s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + b, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("marshal object lock config: %w", err) + } + + err = xattr.Set(path, objectLockKey, b) + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil { + return fmt.Errorf("set object lock config: %w", err) + } + + return nil +} + +func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) { + _, err := os.Stat(bucket) + if errors.Is(err, fs.ErrNotExist) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + } + if err != nil { + return nil, fmt.Errorf("stat bucket: %w", err) + } + + data, err := xattr.Get(filepath.Join(bucket, object), objectLockKey) + if errors.Is(err, meta.ErrNoSuchKey) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration) + } + if err != nil { + return nil, fmt.Errorf("get object lock config: %w", err) + } + + var config auth.ObjectLockConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("pare object lock config: %w", err) + } + + result := &types.ObjectLockLegalHold{} + + if config.LegalHoldEnabled { + result.Status = types.ObjectLockLegalHoldStatusOn + } else { + result.Status = types.ObjectLockLegalHoldStatusOff + } + + return result, nil +} + +func (p *Posix) PutObjectRetention(_ context.Context, input *s3.PutObjectRetentionInput) error { + _, err := os.Stat(*input.Bucket) + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchBucket) + } + if err != nil { + return fmt.Errorf("stat bucket: %w", err) + } + + cfg, err := xattr.Get(*input.Bucket, bucketLockKey) + if errors.Is(err, meta.ErrNoSuchKey) { + return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) + } + if err != nil { + return fmt.Errorf("get object lock config: %w", err) + } + + var bucketLockConfig auth.BucketLockConfig + if err := json.Unmarshal(cfg, &bucketLockConfig); err != nil { + return fmt.Errorf("parse bucket lock config: %w", err) + } + + if !bucketLockConfig.Enabled { + return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) + } + + path := filepath.Join(*input.Bucket, *input.Key) + var config auth.ObjectLockConfig + + data, err := xattr.Get(path, objectLockKey) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if errors.Is(err, meta.ErrNoSuchKey) { + return fmt.Errorf("get object lock config: %w", err) + } + + config = auth.ObjectLockConfig{} + } else { + if err := json.Unmarshal(data, &config); err != nil { + return fmt.Errorf("parse object lock data %w", err) + } + } + + config.Retention = input.Retention + + b, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("marshal object lock config: %w", err) + } + + err = xattr.Set(path, objectLockKey, b) + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil { + return fmt.Errorf("set object lock config: %w", err) + } + + return nil +} + +func (p *Posix) GetObjectRetention(_ context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) { + _, err := os.Stat(bucket) + if errors.Is(err, fs.ErrNotExist) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + } + if err != nil { + return nil, fmt.Errorf("stat bucket: %w", err) + } + + data, err := xattr.Get(filepath.Join(bucket, object), objectLockKey) + if errors.Is(err, meta.ErrNoSuchKey) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration) + } + if err != nil { + return nil, fmt.Errorf("get object lock config: %w", err) + } + + var config auth.ObjectLockConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("pare object lock config: %w", err) + } + + if config.Retention == nil { + return &types.ObjectLockRetention{}, nil + } + + return config.Retention, nil +} + func (p *Posix) ChangeBucketOwner(ctx context.Context, bucket, newOwner string) error { _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index d8b97cb0..f56e6ce3 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -7,6 +7,7 @@ import ( "bufio" "context" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/s3response" "io" @@ -80,6 +81,15 @@ var _ backend.Backend = &BackendMock{} // GetObjectAttributesFunc: func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { // panic("mock out the GetObjectAttributes method") // }, +// GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockLegalHold, error) { +// panic("mock out the GetObjectLegalHold method") +// }, +// GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { +// panic("mock out the GetObjectLockConfiguration method") +// }, +// GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockRetention, error) { +// panic("mock out the GetObjectRetention method") +// }, // GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) { // panic("mock out the GetObjectTagging method") // }, @@ -128,6 +138,15 @@ var _ backend.Backend = &BackendMock{} // PutObjectAclFunc: func(contextMoqParam context.Context, putObjectAclInput *s3.PutObjectAclInput) error { // panic("mock out the PutObjectAcl method") // }, +// PutObjectLegalHoldFunc: func(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error { +// panic("mock out the PutObjectLegalHold method") +// }, +// PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error { +// panic("mock out the PutObjectLockConfiguration method") +// }, +// PutObjectRetentionFunc: func(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error { +// panic("mock out the PutObjectRetention method") +// }, // PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { // panic("mock out the PutObjectTagging method") // }, @@ -213,6 +232,15 @@ type BackendMock struct { // GetObjectAttributesFunc mocks the GetObjectAttributes method. GetObjectAttributesFunc func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) + // GetObjectLegalHoldFunc mocks the GetObjectLegalHold method. + GetObjectLegalHoldFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockLegalHold, error) + + // GetObjectLockConfigurationFunc mocks the GetObjectLockConfiguration method. + GetObjectLockConfigurationFunc func(contextMoqParam context.Context, bucket string) ([]byte, error) + + // GetObjectRetentionFunc mocks the GetObjectRetention method. + GetObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockRetention, error) + // GetObjectTaggingFunc mocks the GetObjectTagging method. GetObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) @@ -261,6 +289,15 @@ type BackendMock struct { // PutObjectAclFunc mocks the PutObjectAcl method. PutObjectAclFunc func(contextMoqParam context.Context, putObjectAclInput *s3.PutObjectAclInput) error + // PutObjectLegalHoldFunc mocks the PutObjectLegalHold method. + PutObjectLegalHoldFunc func(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error + + // PutObjectLockConfigurationFunc mocks the PutObjectLockConfiguration method. + PutObjectLockConfigurationFunc func(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error + + // PutObjectRetentionFunc mocks the PutObjectRetention method. + PutObjectRetentionFunc func(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error + // PutObjectTaggingFunc mocks the PutObjectTagging method. PutObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error @@ -425,6 +462,35 @@ type BackendMock struct { // GetObjectAttributesInput is the getObjectAttributesInput argument value. GetObjectAttributesInput *s3.GetObjectAttributesInput } + // GetObjectLegalHold holds details about calls to the GetObjectLegalHold method. + GetObjectLegalHold []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // Bucket is the bucket argument value. + Bucket string + // Object is the object argument value. + Object string + // VersionId is the versionId argument value. + VersionId string + } + // GetObjectLockConfiguration holds details about calls to the GetObjectLockConfiguration method. + GetObjectLockConfiguration []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // Bucket is the bucket argument value. + Bucket string + } + // GetObjectRetention holds details about calls to the GetObjectRetention method. + GetObjectRetention []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // Bucket is the bucket argument value. + Bucket string + // Object is the object argument value. + Object string + // VersionId is the versionId argument value. + VersionId string + } // GetObjectTagging holds details about calls to the GetObjectTagging method. GetObjectTagging []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -545,6 +611,27 @@ type BackendMock struct { // PutObjectAclInput is the putObjectAclInput argument value. PutObjectAclInput *s3.PutObjectAclInput } + // PutObjectLegalHold holds details about calls to the PutObjectLegalHold method. + PutObjectLegalHold []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // PutObjectLegalHoldInput is the putObjectLegalHoldInput argument value. + PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput + } + // PutObjectLockConfiguration holds details about calls to the PutObjectLockConfiguration method. + PutObjectLockConfiguration []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // PutObjectLockConfigurationInput is the putObjectLockConfigurationInput argument value. + PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput + } + // PutObjectRetention holds details about calls to the PutObjectRetention method. + PutObjectRetention []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // PutObjectRetentionInput is the putObjectRetentionInput argument value. + PutObjectRetentionInput *s3.PutObjectRetentionInput + } // PutObjectTagging holds details about calls to the PutObjectTagging method. PutObjectTagging []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -591,48 +678,54 @@ type BackendMock struct { UploadPartCopyInput *s3.UploadPartCopyInput } } - lockAbortMultipartUpload sync.RWMutex - lockChangeBucketOwner sync.RWMutex - lockCompleteMultipartUpload sync.RWMutex - lockCopyObject sync.RWMutex - lockCreateBucket sync.RWMutex - lockCreateMultipartUpload sync.RWMutex - lockDeleteBucket sync.RWMutex - lockDeleteBucketPolicy sync.RWMutex - lockDeleteBucketTagging sync.RWMutex - lockDeleteObject sync.RWMutex - lockDeleteObjectTagging sync.RWMutex - lockDeleteObjects sync.RWMutex - lockGetBucketAcl sync.RWMutex - lockGetBucketPolicy sync.RWMutex - lockGetBucketTagging sync.RWMutex - lockGetBucketVersioning sync.RWMutex - lockGetObject sync.RWMutex - lockGetObjectAcl sync.RWMutex - lockGetObjectAttributes sync.RWMutex - lockGetObjectTagging sync.RWMutex - lockHeadBucket sync.RWMutex - lockHeadObject sync.RWMutex - lockListBuckets sync.RWMutex - lockListBucketsAndOwners sync.RWMutex - lockListMultipartUploads sync.RWMutex - lockListObjectVersions sync.RWMutex - lockListObjects sync.RWMutex - lockListObjectsV2 sync.RWMutex - lockListParts sync.RWMutex - lockPutBucketAcl sync.RWMutex - lockPutBucketPolicy sync.RWMutex - lockPutBucketTagging sync.RWMutex - lockPutBucketVersioning sync.RWMutex - lockPutObject sync.RWMutex - lockPutObjectAcl sync.RWMutex - lockPutObjectTagging sync.RWMutex - lockRestoreObject sync.RWMutex - lockSelectObjectContent sync.RWMutex - lockShutdown sync.RWMutex - lockString sync.RWMutex - lockUploadPart sync.RWMutex - lockUploadPartCopy sync.RWMutex + lockAbortMultipartUpload sync.RWMutex + lockChangeBucketOwner sync.RWMutex + lockCompleteMultipartUpload sync.RWMutex + lockCopyObject sync.RWMutex + lockCreateBucket sync.RWMutex + lockCreateMultipartUpload sync.RWMutex + lockDeleteBucket sync.RWMutex + lockDeleteBucketPolicy sync.RWMutex + lockDeleteBucketTagging sync.RWMutex + lockDeleteObject sync.RWMutex + lockDeleteObjectTagging sync.RWMutex + lockDeleteObjects sync.RWMutex + lockGetBucketAcl sync.RWMutex + lockGetBucketPolicy sync.RWMutex + lockGetBucketTagging sync.RWMutex + lockGetBucketVersioning sync.RWMutex + lockGetObject sync.RWMutex + lockGetObjectAcl sync.RWMutex + lockGetObjectAttributes sync.RWMutex + lockGetObjectLegalHold sync.RWMutex + lockGetObjectLockConfiguration sync.RWMutex + lockGetObjectRetention sync.RWMutex + lockGetObjectTagging sync.RWMutex + lockHeadBucket sync.RWMutex + lockHeadObject sync.RWMutex + lockListBuckets sync.RWMutex + lockListBucketsAndOwners sync.RWMutex + lockListMultipartUploads sync.RWMutex + lockListObjectVersions sync.RWMutex + lockListObjects sync.RWMutex + lockListObjectsV2 sync.RWMutex + lockListParts sync.RWMutex + lockPutBucketAcl sync.RWMutex + lockPutBucketPolicy sync.RWMutex + lockPutBucketTagging sync.RWMutex + lockPutBucketVersioning sync.RWMutex + lockPutObject sync.RWMutex + lockPutObjectAcl sync.RWMutex + lockPutObjectLegalHold sync.RWMutex + lockPutObjectLockConfiguration sync.RWMutex + lockPutObjectRetention sync.RWMutex + lockPutObjectTagging sync.RWMutex + lockRestoreObject sync.RWMutex + lockSelectObjectContent sync.RWMutex + lockShutdown sync.RWMutex + lockString sync.RWMutex + lockUploadPart sync.RWMutex + lockUploadPartCopy sync.RWMutex } // AbortMultipartUpload calls AbortMultipartUploadFunc. @@ -1335,6 +1428,130 @@ func (mock *BackendMock) GetObjectAttributesCalls() []struct { return calls } +// GetObjectLegalHold calls GetObjectLegalHoldFunc. +func (mock *BackendMock) GetObjectLegalHold(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockLegalHold, error) { + if mock.GetObjectLegalHoldFunc == nil { + panic("BackendMock.GetObjectLegalHoldFunc: method is nil but Backend.GetObjectLegalHold was just called") + } + callInfo := struct { + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + }{ + ContextMoqParam: contextMoqParam, + Bucket: bucket, + Object: object, + VersionId: versionId, + } + mock.lockGetObjectLegalHold.Lock() + mock.calls.GetObjectLegalHold = append(mock.calls.GetObjectLegalHold, callInfo) + mock.lockGetObjectLegalHold.Unlock() + return mock.GetObjectLegalHoldFunc(contextMoqParam, bucket, object, versionId) +} + +// GetObjectLegalHoldCalls gets all the calls that were made to GetObjectLegalHold. +// Check the length with: +// +// len(mockedBackend.GetObjectLegalHoldCalls()) +func (mock *BackendMock) GetObjectLegalHoldCalls() []struct { + ContextMoqParam context.Context + Bucket string + Object string + VersionId string +} { + var calls []struct { + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + } + mock.lockGetObjectLegalHold.RLock() + calls = mock.calls.GetObjectLegalHold + mock.lockGetObjectLegalHold.RUnlock() + return calls +} + +// GetObjectLockConfiguration calls GetObjectLockConfigurationFunc. +func (mock *BackendMock) GetObjectLockConfiguration(contextMoqParam context.Context, bucket string) ([]byte, error) { + if mock.GetObjectLockConfigurationFunc == nil { + panic("BackendMock.GetObjectLockConfigurationFunc: method is nil but Backend.GetObjectLockConfiguration was just called") + } + callInfo := struct { + ContextMoqParam context.Context + Bucket string + }{ + ContextMoqParam: contextMoqParam, + Bucket: bucket, + } + mock.lockGetObjectLockConfiguration.Lock() + mock.calls.GetObjectLockConfiguration = append(mock.calls.GetObjectLockConfiguration, callInfo) + mock.lockGetObjectLockConfiguration.Unlock() + return mock.GetObjectLockConfigurationFunc(contextMoqParam, bucket) +} + +// GetObjectLockConfigurationCalls gets all the calls that were made to GetObjectLockConfiguration. +// Check the length with: +// +// len(mockedBackend.GetObjectLockConfigurationCalls()) +func (mock *BackendMock) GetObjectLockConfigurationCalls() []struct { + ContextMoqParam context.Context + Bucket string +} { + var calls []struct { + ContextMoqParam context.Context + Bucket string + } + mock.lockGetObjectLockConfiguration.RLock() + calls = mock.calls.GetObjectLockConfiguration + mock.lockGetObjectLockConfiguration.RUnlock() + return calls +} + +// GetObjectRetention calls GetObjectRetentionFunc. +func (mock *BackendMock) GetObjectRetention(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockRetention, error) { + if mock.GetObjectRetentionFunc == nil { + panic("BackendMock.GetObjectRetentionFunc: method is nil but Backend.GetObjectRetention was just called") + } + callInfo := struct { + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + }{ + ContextMoqParam: contextMoqParam, + Bucket: bucket, + Object: object, + VersionId: versionId, + } + mock.lockGetObjectRetention.Lock() + mock.calls.GetObjectRetention = append(mock.calls.GetObjectRetention, callInfo) + mock.lockGetObjectRetention.Unlock() + return mock.GetObjectRetentionFunc(contextMoqParam, bucket, object, versionId) +} + +// GetObjectRetentionCalls gets all the calls that were made to GetObjectRetention. +// Check the length with: +// +// len(mockedBackend.GetObjectRetentionCalls()) +func (mock *BackendMock) GetObjectRetentionCalls() []struct { + ContextMoqParam context.Context + Bucket string + Object string + VersionId string +} { + var calls []struct { + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + } + mock.lockGetObjectRetention.RLock() + calls = mock.calls.GetObjectRetention + mock.lockGetObjectRetention.RUnlock() + return calls +} + // GetObjectTagging calls GetObjectTaggingFunc. func (mock *BackendMock) GetObjectTagging(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) { if mock.GetObjectTaggingFunc == nil { @@ -1927,6 +2144,114 @@ func (mock *BackendMock) PutObjectAclCalls() []struct { return calls } +// PutObjectLegalHold calls PutObjectLegalHoldFunc. +func (mock *BackendMock) PutObjectLegalHold(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error { + if mock.PutObjectLegalHoldFunc == nil { + panic("BackendMock.PutObjectLegalHoldFunc: method is nil but Backend.PutObjectLegalHold was just called") + } + callInfo := struct { + ContextMoqParam context.Context + PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput + }{ + ContextMoqParam: contextMoqParam, + PutObjectLegalHoldInput: putObjectLegalHoldInput, + } + mock.lockPutObjectLegalHold.Lock() + mock.calls.PutObjectLegalHold = append(mock.calls.PutObjectLegalHold, callInfo) + mock.lockPutObjectLegalHold.Unlock() + return mock.PutObjectLegalHoldFunc(contextMoqParam, putObjectLegalHoldInput) +} + +// PutObjectLegalHoldCalls gets all the calls that were made to PutObjectLegalHold. +// Check the length with: +// +// len(mockedBackend.PutObjectLegalHoldCalls()) +func (mock *BackendMock) PutObjectLegalHoldCalls() []struct { + ContextMoqParam context.Context + PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput +} { + var calls []struct { + ContextMoqParam context.Context + PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput + } + mock.lockPutObjectLegalHold.RLock() + calls = mock.calls.PutObjectLegalHold + mock.lockPutObjectLegalHold.RUnlock() + return calls +} + +// PutObjectLockConfiguration calls PutObjectLockConfigurationFunc. +func (mock *BackendMock) PutObjectLockConfiguration(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error { + if mock.PutObjectLockConfigurationFunc == nil { + panic("BackendMock.PutObjectLockConfigurationFunc: method is nil but Backend.PutObjectLockConfiguration was just called") + } + callInfo := struct { + ContextMoqParam context.Context + PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput + }{ + ContextMoqParam: contextMoqParam, + PutObjectLockConfigurationInput: putObjectLockConfigurationInput, + } + mock.lockPutObjectLockConfiguration.Lock() + mock.calls.PutObjectLockConfiguration = append(mock.calls.PutObjectLockConfiguration, callInfo) + mock.lockPutObjectLockConfiguration.Unlock() + return mock.PutObjectLockConfigurationFunc(contextMoqParam, putObjectLockConfigurationInput) +} + +// PutObjectLockConfigurationCalls gets all the calls that were made to PutObjectLockConfiguration. +// Check the length with: +// +// len(mockedBackend.PutObjectLockConfigurationCalls()) +func (mock *BackendMock) PutObjectLockConfigurationCalls() []struct { + ContextMoqParam context.Context + PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput +} { + var calls []struct { + ContextMoqParam context.Context + PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput + } + mock.lockPutObjectLockConfiguration.RLock() + calls = mock.calls.PutObjectLockConfiguration + mock.lockPutObjectLockConfiguration.RUnlock() + return calls +} + +// PutObjectRetention calls PutObjectRetentionFunc. +func (mock *BackendMock) PutObjectRetention(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error { + if mock.PutObjectRetentionFunc == nil { + panic("BackendMock.PutObjectRetentionFunc: method is nil but Backend.PutObjectRetention was just called") + } + callInfo := struct { + ContextMoqParam context.Context + PutObjectRetentionInput *s3.PutObjectRetentionInput + }{ + ContextMoqParam: contextMoqParam, + PutObjectRetentionInput: putObjectRetentionInput, + } + mock.lockPutObjectRetention.Lock() + mock.calls.PutObjectRetention = append(mock.calls.PutObjectRetention, callInfo) + mock.lockPutObjectRetention.Unlock() + return mock.PutObjectRetentionFunc(contextMoqParam, putObjectRetentionInput) +} + +// PutObjectRetentionCalls gets all the calls that were made to PutObjectRetention. +// Check the length with: +// +// len(mockedBackend.PutObjectRetentionCalls()) +func (mock *BackendMock) PutObjectRetentionCalls() []struct { + ContextMoqParam context.Context + PutObjectRetentionInput *s3.PutObjectRetentionInput +} { + var calls []struct { + ContextMoqParam context.Context + PutObjectRetentionInput *s3.PutObjectRetentionInput + } + mock.lockPutObjectRetention.RLock() + calls = mock.calls.PutObjectRetention + mock.lockPutObjectRetention.RUnlock() + return calls +} + // PutObjectTagging calls PutObjectTaggingFunc. func (mock *BackendMock) PutObjectTagging(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { if mock.PutObjectTaggingFunc == nil { diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 7bef1bc5..fa6d6328 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -131,6 +131,62 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("retention") { + err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Acl: parsedAcl, + AclPermission: types.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectRetentionAction, + }) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } + + data, err := c.be.GetObjectRetention(ctx.Context(), bucket, key, versionId) + return SendXMLResponse(ctx, data, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } + + if ctx.Request().URI().QueryArgs().Has("legal-hold") { + err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Acl: parsedAcl, + AclPermission: types.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectLegalHoldAction, + }) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectLegalHold", + BucketOwner: parsedAcl.Owner, + }) + } + + data, err := c.be.GetObjectLegalHold(ctx.Context(), bucket, key, versionId) + return SendXMLResponse(ctx, data, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectLegalHold", + BucketOwner: parsedAcl.Owner, + }) + } + if uploadId != "" { if maxParts < 0 && ctx.Request().URI().QueryArgs().Has("max-parts") { return SendResponse(ctx, @@ -547,6 +603,43 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("object-lock") { + err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Acl: parsedAcl, + AclPermission: types.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketObjectLockConfigurationAction, + }) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectLockConfiguration", + BucketOwner: parsedAcl.Owner, + }) + } + + data, err := c.be.GetObjectLockConfiguration(ctx.Context(), bucket) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectLockConfiguration", + BucketOwner: parsedAcl.Owner, + }) + } + + resp, err := auth.ParseBucketLockConfigurationOutput(data) + return SendXMLResponse(ctx, resp, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectLockConfiguration", + BucketOwner: parsedAcl.Owner, + }) + } + if ctx.Request().URI().QueryArgs().Has("acl") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Acl: parsedAcl, @@ -845,6 +938,47 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("object-lock") { + parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + + var input types.ObjectLockConfiguration + if err := xml.Unmarshal(ctx.Body(), &input); err != nil { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), + &MetaOpts{ + Logger: c.logger, + Action: "PutObjectLockConfiguration", + BucketOwner: parsedAcl.Owner, + }) + } + + if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Acl: parsedAcl, + AclPermission: types.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketObjectLockConfigurationAction, + }); err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutObjectLockConfiguration", + BucketOwner: parsedAcl.Owner, + }) + } + + err := c.be.PutObjectLockConfiguration(ctx.Context(), &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &input, + }) + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutObjectLockConfiguration", + BucketOwner: parsedAcl.Owner, + }) + } + if ctx.Request().URI().QueryArgs().Has("policy") { parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ @@ -1076,6 +1210,8 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { keyStart := ctx.Params("key") keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") + versionId := ctx.Query("versionId") + bypassGovernanceRetention := ctx.Get("X-Amz-Bypass-Governance-Retention") acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) @@ -1176,6 +1312,98 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("retention") { + var retention types.ObjectLockRetention + if err := xml.Unmarshal(ctx.Body(), &retention); err != nil { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), &MetaOpts{ + Logger: c.logger, + Action: "PutObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } + + if retention.RetainUntilDate == nil || retention.RetainUntilDate.Before(time.Now()) { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate), + &MetaOpts{ + Logger: c.logger, + Action: "PutObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } + + if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Acl: parsedAcl, + AclPermission: types.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutObjectRetentionAction, + }); err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } + + pass := bypassGovernanceRetention == "True" + + err := c.be.PutObjectRetention(ctx.Context(), &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &keyStart, + VersionId: &versionId, + Retention: &retention, + BypassGovernanceRetention: &pass, + }) + return SendResponse(ctx, err, &MetaOpts{ + Logger: c.logger, + Action: "PutObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } + + if ctx.Request().URI().QueryArgs().Has("legal-hold") { + var legalHold types.ObjectLockLegalHold + if err := xml.Unmarshal(ctx.Body(), &legalHold); err != nil { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), &MetaOpts{ + Logger: c.logger, + Action: "PutObjectLegalHold", + BucketOwner: parsedAcl.Owner, + }) + } + + if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Acl: parsedAcl, + AclPermission: types.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutObjectLegalHoldAction, + }); err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutObjectLegalHold", + BucketOwner: parsedAcl.Owner, + }) + } + + err := c.be.PutObjectLegalHold(ctx.Context(), &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &keyStart, + VersionId: &versionId, + LegalHold: &legalHold, + }) + return SendResponse(ctx, err, &MetaOpts{ + Logger: c.logger, + Action: "PutObjectLegalHold", + BucketOwner: parsedAcl.Owner, + }) + } + if ctx.Request().URI().QueryArgs().Has("uploadId") && ctx.Request().URI().QueryArgs().Has("partNumber") && copySource != "" { @@ -1525,6 +1753,16 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{keyStart}, isRoot || acct.Role == auth.RoleAdmin, c.be) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutObject", + BucketOwner: parsedAcl.Owner, + }) + } + contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64) if err != nil { if c.debug { @@ -1703,6 +1941,16 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { }) } + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, utils.ParseDeleteObjects(dObj.Objects), isRoot || acct.Role == auth.RoleAdmin, c.be) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "DeleteObjects", + BucketOwner: parsedAcl.Owner, + }) + } + res, err := c.be.DeleteObjects(ctx.Context(), &s3.DeleteObjectsInput{ Bucket: &bucket, @@ -1823,6 +2071,16 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { }) } + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{key}, isRoot || acct.Role == auth.RoleAdmin, c.be) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "DeleteObject", + BucketOwner: parsedAcl.Owner, + }) + } + err = c.be.DeleteObject(ctx.Context(), &s3.DeleteObjectInput{ Bucket: &bucket, diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index f34e8656..a9f09a39 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -205,6 +205,12 @@ func TestS3ApiController_GetActions(t *testing.T) { GetObjectTaggingFunc: func(_ context.Context, bucket, object string) (map[string]string, error) { return map[string]string{"hello": "world"}, nil }, + GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) { + return &types.ObjectLockRetention{}, nil + }, + GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) { + return &types.ObjectLockLegalHold{}, nil + }, }, } app.Use(func(ctx *fiber.Ctx) error { @@ -236,6 +242,24 @@ func TestS3ApiController_GetActions(t *testing.T) { wantErr: false, statusCode: 200, }, + { + name: "Get-actions-get-object-retention-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodGet, "/my-bucket/my-obj?retention", nil), + }, + wantErr: false, + statusCode: 200, + }, + { + name: "Get-actions-get-object-legal-hold-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodGet, "/my-bucket/my-obj?legal-hold", nil), + }, + wantErr: false, + statusCode: 200, + }, { name: "Get-actions-invalid-max-parts-string", app: app, @@ -329,6 +353,11 @@ func TestS3ApiController_ListActions(t *testing.T) { req *http.Request } + objectLockResult, err := json.Marshal(auth.BucketLockConfig{}) + if err != nil { + t.Errorf("failed to parse object lock result %v", err) + } + app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ @@ -356,6 +385,9 @@ func TestS3ApiController_ListActions(t *testing.T) { GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { return []byte{}, nil }, + GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return objectLockResult, nil + }, }, } @@ -369,7 +401,7 @@ func TestS3ApiController_ListActions(t *testing.T) { app.Get("/:bucket", s3ApiController.ListActions) - //Error case + // Error case s3ApiControllerError := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { @@ -418,6 +450,15 @@ func TestS3ApiController_ListActions(t *testing.T) { wantErr: false, statusCode: 200, }, + { + name: "Get-object-lock-configuration-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodGet, "/my-bucket?object-lock", nil), + }, + wantErr: false, + statusCode: 200, + }, { name: "Get-bucket-acl-success", app: app, @@ -584,6 +625,18 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { } ` + objectLockBody := ` + + Enabled + + + GOVERNANCE + 2 + + + + ` + s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { @@ -604,6 +657,9 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { PutBucketPolicyFunc: func(contextMoqParam context.Context, bucket string, policy []byte) error { return nil }, + PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error { + return nil + }, }, } // Mock ctx.Locals @@ -662,6 +718,24 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { wantErr: false, statusCode: 200, }, + { + name: "Put-object-lock-configuration-invalid-body", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket?object-lock", nil), + }, + wantErr: false, + statusCode: 400, + }, + { + name: "Put-object-lock-configuration-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket?object-lock", strings.NewReader(objectLockBody)), + }, + wantErr: false, + statusCode: 200, + }, { name: "Put-bucket-versioning-invalid-body", app: app, @@ -806,6 +880,19 @@ func TestS3ApiController_PutActions(t *testing.T) { ` + retentionBody := ` + + GOVERNANCE + 2025-01-01T00:00:00Z + + ` + + legalHoldBody := ` + + string + + ` + app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ @@ -832,6 +919,15 @@ func TestS3ApiController_PutActions(t *testing.T) { UploadPartCopyFunc: func(context.Context, *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) { return s3response.CopyObjectResult{}, nil }, + PutObjectLegalHoldFunc: func(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error { + return nil + }, + PutObjectRetentionFunc: func(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error { + return nil + }, + GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + }, }, } app.Use(func(ctx *fiber.Ctx) error { @@ -910,6 +1006,42 @@ func TestS3ApiController_PutActions(t *testing.T) { wantErr: false, statusCode: 200, }, + { + name: "put-object-retention-invalid-request", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?retention", nil), + }, + wantErr: false, + statusCode: 400, + }, + { + name: "put-object-retention-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?retention", strings.NewReader(retentionBody)), + }, + wantErr: false, + statusCode: 200, + }, + { + name: "put-legal-hold-invalid-request", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?legal-hold", nil), + }, + wantErr: false, + statusCode: 400, + }, + { + name: "put-legal-hold-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?legal-hold", strings.NewReader(legalHoldBody)), + }, + wantErr: false, + statusCode: 200, + }, { name: "Put-object-acl-invalid-acl", app: app, @@ -1096,6 +1228,9 @@ func TestS3ApiController_DeleteObjects(t *testing.T) { DeleteObjectsFunc: func(context.Context, *s3.DeleteObjectsInput) (s3response.DeleteResult, error) { return s3response.DeleteResult{}, nil }, + GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + }, }, } @@ -1173,6 +1308,9 @@ func TestS3ApiController_DeleteActions(t *testing.T) { DeleteObjectTaggingFunc: func(_ context.Context, bucket, object string) error { return nil }, + GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + }, }, } @@ -1195,6 +1333,9 @@ func TestS3ApiController_DeleteActions(t *testing.T) { DeleteObjectFunc: func(context.Context, *s3.DeleteObjectInput) error { return s3err.GetAPIError(7) }, + GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + }, }} appErr.Use(func(ctx *fiber.Ctx) error { diff --git a/s3api/middlewares/acl-parser.go b/s3api/middlewares/acl-parser.go index 54446554..b966bbea 100644 --- a/s3api/middlewares/acl-parser.go +++ b/s3api/middlewares/acl-parser.go @@ -48,7 +48,8 @@ func AclParser(be backend.Backend, logger s3log.AuditLogger) fiber.Handler { !ctx.Request().URI().QueryArgs().Has("acl") && !ctx.Request().URI().QueryArgs().Has("tagging") && !ctx.Request().URI().QueryArgs().Has("versioning") && - !ctx.Request().URI().QueryArgs().Has("policy") { + !ctx.Request().URI().QueryArgs().Has("policy") && + !ctx.Request().URI().QueryArgs().Has("object-lock") { if err := auth.MayCreateBucket(acct, isRoot); err != nil { return controllers.SendXMLResponse(ctx, nil, err, &controllers.MetaOpts{Logger: logger, Action: "CreateBucket"}) } diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index f0241a3a..17e9be59 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -26,6 +26,7 @@ import ( "strings" "time" + "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go/encoding/httpbinding" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" @@ -244,3 +245,11 @@ func ValidateDate(date time.Time) error { return nil } + +func ParseDeleteObjects(objs []types.ObjectIdentifier) (result []string) { + for _, obj := range objs { + result = append(result, *obj.Key) + } + + return +} diff --git a/s3err/s3err.go b/s3err/s3err.go index aa3d11af..35b76765 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -111,6 +111,11 @@ const ( ErrInvalidObjectState ErrInvalidRange ErrInvalidURI + ErrObjectLockConfigurationNotFound + ErrNoSuchObjectLockConfiguration + ErrInvalidBucketObjectLockConfiguration + ErrObjectLocked + ErrPastObjectLockRetainDate // Non-AWS errors ErrExistingObjectIsDirectory @@ -400,6 +405,31 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The specified URI couldn't be parsed.", HTTPStatusCode: http.StatusBadRequest, }, + ErrObjectLockConfigurationNotFound: { + Code: "ObjectLockConfigurationNotFoundError", + Description: "Object Lock configuration does not exist for this bucket", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchObjectLockConfiguration: { + Code: "NoSuchObjectLockConfiguration", + Description: "The specified object does not have an ObjectLock configuration", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidBucketObjectLockConfiguration: { + Code: "InvalidRequest", + Description: "Bucket is missing ObjectLockConfiguration", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrObjectLocked: { + Code: "InvalidRequest", + Description: "Object is WORM protected and cannot be overwritten", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrPastObjectLockRetainDate: { + Code: "InvalidRequest", + Description: "the retain until date must be in the future", + HTTPStatusCode: http.StatusBadRequest, + }, ErrExistingObjectIsDirectory: { Code: "ExistingObjectIsDirectory", Description: "Existing Object is a directory.", From 00476ef70c6be3a4da30eff679d1158e96f05b51 Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Wed, 17 Apr 2024 17:35:35 -0400 Subject: [PATCH 2/3] feat: Closes #490, Added integration tests for object lock actions --- auth/object_lock.go | 8 +- backend/posix/posix.go | 6 + tests/integration/group-tests.go | 472 ++++++++------ tests/integration/tests.go | 1011 ++++++++++++++++++++++++++++++ tests/integration/utils.go | 59 ++ 5 files changed, 1367 insertions(+), 189 deletions(-) diff --git a/auth/object_lock.go b/auth/object_lock.go index 6882790c..4a1f1b21 100644 --- a/auth/object_lock.go +++ b/auth/object_lock.go @@ -75,10 +75,13 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ return nil } + objExists := true + for _, obj := range objects { retention, err := be.GetObjectRetention(ctx, bucket, obj, "") if err != nil { if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { + objExists = false continue } if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) { @@ -97,6 +100,9 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ if err != nil { return err } + if len(policy) == 0 { + return s3err.GetAPIError(s3err.ErrObjectLocked) + } err = verifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) if err != nil { return s3err.GetAPIError(s3err.ErrObjectLocked) @@ -118,7 +124,7 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ } } - if bucketLockConfig.DefaultRetention != nil && bucketLockConfig.CreatedAt != nil { + if bucketLockConfig.DefaultRetention != nil && bucketLockConfig.CreatedAt != nil && objExists { expirationDate := *bucketLockConfig.CreatedAt if bucketLockConfig.DefaultRetention.Days != nil { expirationDate = expirationDate.AddDate(0, 0, int(*bucketLockConfig.DefaultRetention.Days)) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 3fc6e320..7ce5abb9 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -2153,6 +2153,9 @@ func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId } data, err := xattr.Get(filepath.Join(bucket, object), objectLockKey) + if errors.Is(err, fs.ErrNotExist) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } if errors.Is(err, meta.ErrNoSuchKey) { return nil, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration) } @@ -2249,6 +2252,9 @@ func (p *Posix) GetObjectRetention(_ context.Context, bucket, object, versionId } data, err := xattr.Get(filepath.Join(bucket, object), objectLockKey) + if errors.Is(err, fs.ErrNotExist) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } if errors.Is(err, meta.ErrNoSuchKey) { return nil, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration) } diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index a5cb6dd5..a8ad3516 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -277,6 +277,61 @@ func TestDeleteBucketPolicy(s *S3Conf) { DeleteBucketPolicy_success(s) } +func TestPutObjectLockConfiguration(s *S3Conf) { + PutObjectLockConfiguration_non_existing_bucket(s) + PutObjectLockConfiguration_empty_config(s) + PutObjectLockConfiguration_both_years_and_days(s) + PutObjectLockConfiguration_success(s) +} + +func TestGetObjectLockConfiguration(s *S3Conf) { + GetObjectLockConfiguration_non_existing_bucket(s) + GetObjectLockConfiguration_unset_config(s) + GetObjectLockConfiguration_success(s) +} + +func TestPutObjectRetention(s *S3Conf) { + PutObjectRetention_non_existing_bucket(s) + PutObjectRetention_non_existing_object(s) + PutObjectRetention_unset_bucket_object_lock_config(s) + PutObjectRetention_disabled_bucket_object_lock_config(s) + PutObjectRetention_expired_retain_until_date(s) + PutObjectRetention_success(s) +} + +func TestGetObjectRetention(s *S3Conf) { + GetObjectRetention_non_existing_bucket(s) + GetObjectRetention_non_existing_object(s) + GetObjectRetention_unset_config(s) + GetObjectRetention_success(s) +} + +func TestPutObjectLegalHold(s *S3Conf) { + PutObjectLegalHold_non_existing_bucket(s) + PutObjectLegalHold_non_existing_object(s) + PutObjectLegalHold_invalid_body(s) + PutObjectLegalHold_unset_bucket_object_lock_config(s) + PutObjectLegalHold_disabled_bucket_object_lock_config(s) + PutObjectLegalHold_success(s) +} + +func TestGetObjectLegalHold(s *S3Conf) { + GetObjectLegalHold_non_existing_bucket(s) + GetObjectLegalHold_non_existing_object(s) + GetObjectLegalHold_unset_config(s) + GetObjectLegalHold_success(s) +} + +func TestWORMProtection(s *S3Conf) { + WORMProtection_bucket_object_lock_configuration_compliance_mode(s) + WORMProtection_bucket_object_lock_governance_root_overwrite(s) + WORMProtection_object_lock_retention_compliance_root_access_denied(s) + WORMProtection_object_lock_retention_governance_root_overwrite(s) + WORMProtection_object_lock_retention_governance_user_access_denied(s) + WORMProtection_object_lock_legal_hold_user_access_denied(s) + WORMProtection_object_lock_legal_hold_root_overwrite(s) +} + func TestFullFlow(s *S3Conf) { TestAuthentication(s) TestPresignedAuthentication(s) @@ -309,6 +364,13 @@ func TestFullFlow(s *S3Conf) { TestPutBucketPolicy(s) TestGetBucketPolicy(s) TestDeleteBucketPolicy(s) + TestPutObjectLockConfiguration(s) + TestGetObjectLockConfiguration(s) + TestPutObjectRetention(s) + TestGetObjectRetention(s) + TestPutObjectLegalHold(s) + TestGetObjectLegalHold(s) + TestWORMProtection(s) TestAccessControl(s) } @@ -340,193 +402,227 @@ type IntTests map[string]func(s *S3Conf) error func GetIntTests() IntTests { return IntTests{ - "Authentication_empty_auth_header": Authentication_empty_auth_header, - "Authentication_invalid_auth_header": Authentication_invalid_auth_header, - "Authentication_unsupported_signature_version": Authentication_unsupported_signature_version, - "Authentication_malformed_credentials": Authentication_malformed_credentials, - "Authentication_malformed_credentials_invalid_parts": Authentication_malformed_credentials_invalid_parts, - "Authentication_credentials_terminated_string": Authentication_credentials_terminated_string, - "Authentication_credentials_incorrect_service": Authentication_credentials_incorrect_service, - "Authentication_credentials_incorrect_region": Authentication_credentials_incorrect_region, - "Authentication_credentials_invalid_date": Authentication_credentials_invalid_date, - "Authentication_credentials_future_date": Authentication_credentials_future_date, - "Authentication_credentials_past_date": Authentication_credentials_past_date, - "Authentication_credentials_non_existing_access_key": Authentication_credentials_non_existing_access_key, - "Authentication_invalid_signed_headers": Authentication_invalid_signed_headers, - "Authentication_missing_date_header": Authentication_missing_date_header, - "Authentication_invalid_date_header": Authentication_invalid_date_header, - "Authentication_date_mismatch": Authentication_date_mismatch, - "Authentication_incorrect_payload_hash": Authentication_incorrect_payload_hash, - "Authentication_incorrect_md5": Authentication_incorrect_md5, - "Authentication_signature_error_incorrect_secret_key": Authentication_signature_error_incorrect_secret_key, - "PresignedAuth_missing_algo_query_param": PresignedAuth_missing_algo_query_param, - "PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm, - "PresignedAuth_missing_credentials_query_param": PresignedAuth_missing_credentials_query_param, - "PresignedAuth_malformed_creds_invalid_parts": PresignedAuth_malformed_creds_invalid_parts, - "PresignedAuth_creds_invalid_terminator": PresignedAuth_creds_invalid_terminator, - "PresignedAuth_creds_incorrect_service": PresignedAuth_creds_incorrect_service, - "PresignedAuth_creds_incorrect_region": PresignedAuth_creds_incorrect_region, - "PresignedAuth_creds_invalid_date": PresignedAuth_creds_invalid_date, - "PresignedAuth_missing_date_query": PresignedAuth_missing_date_query, - "PresignedAuth_dates_mismatch": PresignedAuth_dates_mismatch, - "PresignedAuth_non_existing_access_key_id": PresignedAuth_non_existing_access_key_id, - "PresignedAuth_missing_signed_headers_query_param": PresignedAuth_missing_signed_headers_query_param, - "PresignedAuth_missing_expiration_query_param": PresignedAuth_missing_expiration_query_param, - "PresignedAuth_invalid_expiration_query_param": PresignedAuth_invalid_expiration_query_param, - "PresignedAuth_negative_expiration_query_param": PresignedAuth_negative_expiration_query_param, - "PresignedAuth_exceeding_expiration_query_param": PresignedAuth_exceeding_expiration_query_param, - "PresignedAuth_expired_request": PresignedAuth_expired_request, - "PresignedAuth_incorrect_secret_key": PresignedAuth_incorrect_secret_key, - "PresignedAuth_PutObject_success": PresignedAuth_PutObject_success, - "PresignedAuth_Put_GetObject_with_data": PresignedAuth_Put_GetObject_with_data, - "PresignedAuth_Put_GetObject_with_UTF8_chars": PresignedAuth_Put_GetObject_with_UTF8_chars, - "PresignedAuth_UploadPart": PresignedAuth_UploadPart, - "CreateBucket_invalid_bucket_name": CreateBucket_invalid_bucket_name, - "CreateBucket_existing_bucket": CreateBucket_existing_bucket, - "CreateBucket_as_user": CreateBucket_as_user, - "CreateDeleteBucket_success": CreateDeleteBucket_success, - "CreateBucket_default_acl": CreateBucket_default_acl, - "CreateBucket_non_default_acl": CreateBucket_non_default_acl, - "HeadBucket_non_existing_bucket": HeadBucket_non_existing_bucket, - "HeadBucket_success": HeadBucket_success, - "ListBuckets_as_user": ListBuckets_as_user, - "ListBuckets_as_admin": ListBuckets_as_admin, - "ListBuckets_success": ListBuckets_success, - "DeleteBucket_non_existing_bucket": DeleteBucket_non_existing_bucket, - "DeleteBucket_non_empty_bucket": DeleteBucket_non_empty_bucket, - "DeleteBucket_success_status_code": DeleteBucket_success_status_code, - "PutBucketTagging_non_existing_bucket": PutBucketTagging_non_existing_bucket, - "PutBucketTagging_long_tags": PutBucketTagging_long_tags, - "PutBucketTagging_success": PutBucketTagging_success, - "GetBucketTagging_non_existing_bucket": GetBucketTagging_non_existing_bucket, - "GetBucketTagging_success": GetBucketTagging_success, - "DeleteBucketTagging_non_existing_object": DeleteBucketTagging_non_existing_object, - "DeleteBucketTagging_success_status": DeleteBucketTagging_success_status, - "DeleteBucketTagging_success": DeleteBucketTagging_success, - "PutObject_non_existing_bucket": PutObject_non_existing_bucket, - "PutObject_special_chars": PutObject_special_chars, - "PutObject_invalid_long_tags": PutObject_invalid_long_tags, - "PutObject_success": PutObject_success, - "HeadObject_non_existing_object": HeadObject_non_existing_object, - "HeadObject_success": HeadObject_success, - "GetObject_non_existing_key": GetObject_non_existing_key, - "GetObject_invalid_ranges": GetObject_invalid_ranges, - "GetObject_with_meta": GetObject_with_meta, - "GetObject_success": GetObject_success, - "GetObject_by_range_success": GetObject_by_range_success, - "ListObjects_non_existing_bucket": ListObjects_non_existing_bucket, - "ListObjects_with_prefix": ListObjects_with_prefix, - "ListObject_truncated": ListObject_truncated, - "ListObjects_invalid_max_keys": ListObjects_invalid_max_keys, - "ListObjects_max_keys_0": ListObjects_max_keys_0, - "ListObjects_delimiter": ListObjects_delimiter, - "ListObjects_max_keys_none": ListObjects_max_keys_none, - "ListObjects_marker_not_from_obj_list": ListObjects_marker_not_from_obj_list, - "ListObjectsV2_start_after": ListObjectsV2_start_after, - "ListObjectsV2_both_start_after_and_continuation_token": ListObjectsV2_both_start_after_and_continuation_token, - "ListObjectsV2_start_after_not_in_list": ListObjectsV2_start_after_not_in_list, - "ListObjectsV2_start_after_empty_result": ListObjectsV2_start_after_empty_result, - "DeleteObject_non_existing_object": DeleteObject_non_existing_object, - "DeleteObject_success": DeleteObject_success, - "DeleteObject_success_status_code": DeleteObject_success_status_code, - "DeleteObjects_empty_input": DeleteObjects_empty_input, - "DeleteObjects_non_existing_objects": DeleteObjects_non_existing_objects, - "DeleteObjects_success": DeleteObjects_success, - "CopyObject_non_existing_dst_bucket": CopyObject_non_existing_dst_bucket, - "CopyObject_not_owned_source_bucket": CopyObject_not_owned_source_bucket, - "CopyObject_copy_to_itself": CopyObject_copy_to_itself, - "CopyObject_to_itself_with_new_metadata": CopyObject_to_itself_with_new_metadata, - "CopyObject_success": CopyObject_success, - "PutObjectTagging_non_existing_object": PutObjectTagging_non_existing_object, - "PutObjectTagging_long_tags": PutObjectTagging_long_tags, - "PutObjectTagging_success": PutObjectTagging_success, - "GetObjectTagging_non_existing_object": GetObjectTagging_non_existing_object, - "GetObjectTagging_success": GetObjectTagging_success, - "DeleteObjectTagging_non_existing_object": DeleteObjectTagging_non_existing_object, - "DeleteObjectTagging_success_status": DeleteObjectTagging_success_status, - "DeleteObjectTagging_success": DeleteObjectTagging_success, - "CreateMultipartUpload_non_existing_bucket": CreateMultipartUpload_non_existing_bucket, - "CreateMultipartUpload_success": CreateMultipartUpload_success, - "UploadPart_non_existing_bucket": UploadPart_non_existing_bucket, - "UploadPart_invalid_part_number": UploadPart_invalid_part_number, - "UploadPart_non_existing_key": UploadPart_non_existing_key, - "UploadPart_non_existing_mp_upload": UploadPart_non_existing_mp_upload, - "UploadPart_success": UploadPart_success, - "UploadPartCopy_non_existing_bucket": UploadPartCopy_non_existing_bucket, - "UploadPartCopy_incorrect_uploadId": UploadPartCopy_incorrect_uploadId, - "UploadPartCopy_incorrect_object_key": UploadPartCopy_incorrect_object_key, - "UploadPartCopy_invalid_part_number": UploadPartCopy_invalid_part_number, - "UploadPartCopy_invalid_copy_source": UploadPartCopy_invalid_copy_source, - "UploadPartCopy_non_existing_source_bucket": UploadPartCopy_non_existing_source_bucket, - "UploadPartCopy_non_existing_source_object_key": UploadPartCopy_non_existing_source_object_key, - "UploadPartCopy_success": UploadPartCopy_success, - "UploadPartCopy_by_range_invalid_range": UploadPartCopy_by_range_invalid_range, - "UploadPartCopy_greater_range_than_obj_size": UploadPartCopy_greater_range_than_obj_size, - "UploadPartCopy_by_range_success": UploadPartCopy_by_range_success, - "ListParts_incorrect_uploadId": ListParts_incorrect_uploadId, - "ListParts_incorrect_object_key": ListParts_incorrect_object_key, - "ListParts_success": ListParts_success, - "ListMultipartUploads_non_existing_bucket": ListMultipartUploads_non_existing_bucket, - "ListMultipartUploads_empty_result": ListMultipartUploads_empty_result, - "ListMultipartUploads_invalid_max_uploads": ListMultipartUploads_invalid_max_uploads, - "ListMultipartUploads_max_uploads": ListMultipartUploads_max_uploads, - "ListMultipartUploads_incorrect_next_key_marker": ListMultipartUploads_incorrect_next_key_marker, - "ListMultipartUploads_ignore_upload_id_marker": ListMultipartUploads_ignore_upload_id_marker, - "ListMultipartUploads_success": ListMultipartUploads_success, - "AbortMultipartUpload_non_existing_bucket": AbortMultipartUpload_non_existing_bucket, - "AbortMultipartUpload_incorrect_uploadId": AbortMultipartUpload_incorrect_uploadId, - "AbortMultipartUpload_incorrect_object_key": AbortMultipartUpload_incorrect_object_key, - "AbortMultipartUpload_success": AbortMultipartUpload_success, - "AbortMultipartUpload_success_status_code": AbortMultipartUpload_success_status_code, - "CompletedMultipartUpload_non_existing_bucket": CompletedMultipartUpload_non_existing_bucket, - "CompleteMultipartUpload_invalid_part_number": CompleteMultipartUpload_invalid_part_number, - "CompleteMultipartUpload_invalid_ETag": CompleteMultipartUpload_invalid_ETag, - "CompleteMultipartUpload_success": CompleteMultipartUpload_success, - "PutBucketAcl_non_existing_bucket": PutBucketAcl_non_existing_bucket, - "PutBucketAcl_invalid_acl_canned_and_acp": PutBucketAcl_invalid_acl_canned_and_acp, - "PutBucketAcl_invalid_acl_canned_and_grants": PutBucketAcl_invalid_acl_canned_and_grants, - "PutBucketAcl_invalid_acl_acp_and_grants": PutBucketAcl_invalid_acl_acp_and_grants, - "PutBucketAcl_invalid_owner": PutBucketAcl_invalid_owner, - "PutBucketAcl_success_access_denied": PutBucketAcl_success_access_denied, - "PutBucketAcl_success_grants": PutBucketAcl_success_grants, - "PutBucketAcl_success_canned_acl": PutBucketAcl_success_canned_acl, - "PutBucketAcl_success_acp": PutBucketAcl_success_acp, - "GetBucketAcl_non_existing_bucket": GetBucketAcl_non_existing_bucket, - "GetBucketAcl_access_denied": GetBucketAcl_access_denied, - "GetBucketAcl_success": GetBucketAcl_success, - "PutBucketPolicy_non_existing_bucket": PutBucketPolicy_non_existing_bucket, - "PutBucketPolicy_invalid_effect": PutBucketPolicy_invalid_effect, - "PutBucketPolicy_empty_actions_string": PutBucketPolicy_empty_actions_string, - "PutBucketPolicy_empty_actions_array": PutBucketPolicy_empty_actions_array, - "PutBucketPolicy_invalid_action": PutBucketPolicy_invalid_action, - "PutBucketPolicy_unsupported_action": PutBucketPolicy_unsupported_action, - "PutBucketPolicy_incorrect_action_wildcard_usage": PutBucketPolicy_incorrect_action_wildcard_usage, - "PutBucketPolicy_empty_principals_string": PutBucketPolicy_empty_principals_string, - "PutBucketPolicy_empty_principals_array": PutBucketPolicy_empty_principals_array, - "PutBucketPolicy_principals_incorrect_wildcard_usage": PutBucketPolicy_principals_incorrect_wildcard_usage, - "PutBucketPolicy_non_existing_principals": PutBucketPolicy_non_existing_principals, - "PutBucketPolicy_empty_resources_string": PutBucketPolicy_empty_resources_string, - "PutBucketPolicy_empty_resources_array": PutBucketPolicy_empty_resources_array, - "PutBucketPolicy_invalid_resource_prefix": PutBucketPolicy_invalid_resource_prefix, - "PutBucketPolicy_invalid_resource_with_starting_slash": PutBucketPolicy_invalid_resource_with_starting_slash, - "PutBucketPolicy_duplicate_resource": PutBucketPolicy_duplicate_resource, - "PutBucketPolicy_incorrect_bucket_name": PutBucketPolicy_incorrect_bucket_name, - "PutBucketPolicy_object_action_on_bucket_resource": PutBucketPolicy_object_action_on_bucket_resource, - "PutBucketPolicy_bucket_action_on_object_resource": PutBucketPolicy_bucket_action_on_object_resource, - "PutBucketPolicy_success": PutBucketPolicy_success, - "GetBucketPolicy_non_existing_bucket": GetBucketPolicy_non_existing_bucket, - "GetBucketPolicy_default_empty_policy": GetBucketPolicy_default_empty_policy, - "GetBucketPolicy_success": GetBucketPolicy_success, - "DeleteBucketPolicy_non_existing_bucket": DeleteBucketPolicy_non_existing_bucket, - "DeleteBucketPolicy_remove_before_setting": DeleteBucketPolicy_remove_before_setting, - "DeleteBucketPolicy_success": DeleteBucketPolicy_success, - "PutObject_overwrite_dir_obj": PutObject_overwrite_dir_obj, - "PutObject_overwrite_file_obj": PutObject_overwrite_file_obj, - "PutObject_dir_obj_with_data": PutObject_dir_obj_with_data, - "CreateMultipartUpload_dir_obj": CreateMultipartUpload_dir_obj, - "IAM_user_access_denied": IAM_user_access_denied, - "IAM_userplus_access_denied": IAM_userplus_access_denied, - "IAM_userplus_CreateBucket": IAM_userplus_CreateBucket, - "IAM_admin_ChangeBucketOwner": IAM_admin_ChangeBucketOwner, + "Authentication_empty_auth_header": Authentication_empty_auth_header, + "Authentication_invalid_auth_header": Authentication_invalid_auth_header, + "Authentication_unsupported_signature_version": Authentication_unsupported_signature_version, + "Authentication_malformed_credentials": Authentication_malformed_credentials, + "Authentication_malformed_credentials_invalid_parts": Authentication_malformed_credentials_invalid_parts, + "Authentication_credentials_terminated_string": Authentication_credentials_terminated_string, + "Authentication_credentials_incorrect_service": Authentication_credentials_incorrect_service, + "Authentication_credentials_incorrect_region": Authentication_credentials_incorrect_region, + "Authentication_credentials_invalid_date": Authentication_credentials_invalid_date, + "Authentication_credentials_future_date": Authentication_credentials_future_date, + "Authentication_credentials_past_date": Authentication_credentials_past_date, + "Authentication_credentials_non_existing_access_key": Authentication_credentials_non_existing_access_key, + "Authentication_invalid_signed_headers": Authentication_invalid_signed_headers, + "Authentication_missing_date_header": Authentication_missing_date_header, + "Authentication_invalid_date_header": Authentication_invalid_date_header, + "Authentication_date_mismatch": Authentication_date_mismatch, + "Authentication_incorrect_payload_hash": Authentication_incorrect_payload_hash, + "Authentication_incorrect_md5": Authentication_incorrect_md5, + "Authentication_signature_error_incorrect_secret_key": Authentication_signature_error_incorrect_secret_key, + "PresignedAuth_missing_algo_query_param": PresignedAuth_missing_algo_query_param, + "PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm, + "PresignedAuth_missing_credentials_query_param": PresignedAuth_missing_credentials_query_param, + "PresignedAuth_malformed_creds_invalid_parts": PresignedAuth_malformed_creds_invalid_parts, + "PresignedAuth_creds_invalid_terminator": PresignedAuth_creds_invalid_terminator, + "PresignedAuth_creds_incorrect_service": PresignedAuth_creds_incorrect_service, + "PresignedAuth_creds_incorrect_region": PresignedAuth_creds_incorrect_region, + "PresignedAuth_creds_invalid_date": PresignedAuth_creds_invalid_date, + "PresignedAuth_missing_date_query": PresignedAuth_missing_date_query, + "PresignedAuth_dates_mismatch": PresignedAuth_dates_mismatch, + "PresignedAuth_non_existing_access_key_id": PresignedAuth_non_existing_access_key_id, + "PresignedAuth_missing_signed_headers_query_param": PresignedAuth_missing_signed_headers_query_param, + "PresignedAuth_missing_expiration_query_param": PresignedAuth_missing_expiration_query_param, + "PresignedAuth_invalid_expiration_query_param": PresignedAuth_invalid_expiration_query_param, + "PresignedAuth_negative_expiration_query_param": PresignedAuth_negative_expiration_query_param, + "PresignedAuth_exceeding_expiration_query_param": PresignedAuth_exceeding_expiration_query_param, + "PresignedAuth_expired_request": PresignedAuth_expired_request, + "PresignedAuth_incorrect_secret_key": PresignedAuth_incorrect_secret_key, + "PresignedAuth_PutObject_success": PresignedAuth_PutObject_success, + "PresignedAuth_Put_GetObject_with_data": PresignedAuth_Put_GetObject_with_data, + "PresignedAuth_Put_GetObject_with_UTF8_chars": PresignedAuth_Put_GetObject_with_UTF8_chars, + "PresignedAuth_UploadPart": PresignedAuth_UploadPart, + "CreateBucket_invalid_bucket_name": CreateBucket_invalid_bucket_name, + "CreateBucket_existing_bucket": CreateBucket_existing_bucket, + "CreateBucket_as_user": CreateBucket_as_user, + "CreateDeleteBucket_success": CreateDeleteBucket_success, + "CreateBucket_default_acl": CreateBucket_default_acl, + "CreateBucket_non_default_acl": CreateBucket_non_default_acl, + "HeadBucket_non_existing_bucket": HeadBucket_non_existing_bucket, + "HeadBucket_success": HeadBucket_success, + "ListBuckets_as_user": ListBuckets_as_user, + "ListBuckets_as_admin": ListBuckets_as_admin, + "ListBuckets_success": ListBuckets_success, + "DeleteBucket_non_existing_bucket": DeleteBucket_non_existing_bucket, + "DeleteBucket_non_empty_bucket": DeleteBucket_non_empty_bucket, + "DeleteBucket_success_status_code": DeleteBucket_success_status_code, + "PutBucketTagging_non_existing_bucket": PutBucketTagging_non_existing_bucket, + "PutBucketTagging_long_tags": PutBucketTagging_long_tags, + "PutBucketTagging_success": PutBucketTagging_success, + "GetBucketTagging_non_existing_bucket": GetBucketTagging_non_existing_bucket, + "GetBucketTagging_success": GetBucketTagging_success, + "DeleteBucketTagging_non_existing_object": DeleteBucketTagging_non_existing_object, + "DeleteBucketTagging_success_status": DeleteBucketTagging_success_status, + "DeleteBucketTagging_success": DeleteBucketTagging_success, + "PutObject_non_existing_bucket": PutObject_non_existing_bucket, + "PutObject_special_chars": PutObject_special_chars, + "PutObject_invalid_long_tags": PutObject_invalid_long_tags, + "PutObject_success": PutObject_success, + "HeadObject_non_existing_object": HeadObject_non_existing_object, + "HeadObject_success": HeadObject_success, + "GetObject_non_existing_key": GetObject_non_existing_key, + "GetObject_invalid_ranges": GetObject_invalid_ranges, + "GetObject_with_meta": GetObject_with_meta, + "GetObject_success": GetObject_success, + "GetObject_by_range_success": GetObject_by_range_success, + "ListObjects_non_existing_bucket": ListObjects_non_existing_bucket, + "ListObjects_with_prefix": ListObjects_with_prefix, + "ListObject_truncated": ListObject_truncated, + "ListObjects_invalid_max_keys": ListObjects_invalid_max_keys, + "ListObjects_max_keys_0": ListObjects_max_keys_0, + "ListObjects_delimiter": ListObjects_delimiter, + "ListObjects_max_keys_none": ListObjects_max_keys_none, + "ListObjects_marker_not_from_obj_list": ListObjects_marker_not_from_obj_list, + "ListObjectsV2_start_after": ListObjectsV2_start_after, + "ListObjectsV2_both_start_after_and_continuation_token": ListObjectsV2_both_start_after_and_continuation_token, + "ListObjectsV2_start_after_not_in_list": ListObjectsV2_start_after_not_in_list, + "ListObjectsV2_start_after_empty_result": ListObjectsV2_start_after_empty_result, + "DeleteObject_non_existing_object": DeleteObject_non_existing_object, + "DeleteObject_success": DeleteObject_success, + "DeleteObject_success_status_code": DeleteObject_success_status_code, + "DeleteObjects_empty_input": DeleteObjects_empty_input, + "DeleteObjects_non_existing_objects": DeleteObjects_non_existing_objects, + "DeleteObjects_success": DeleteObjects_success, + "CopyObject_non_existing_dst_bucket": CopyObject_non_existing_dst_bucket, + "CopyObject_not_owned_source_bucket": CopyObject_not_owned_source_bucket, + "CopyObject_copy_to_itself": CopyObject_copy_to_itself, + "CopyObject_to_itself_with_new_metadata": CopyObject_to_itself_with_new_metadata, + "CopyObject_success": CopyObject_success, + "PutObjectTagging_non_existing_object": PutObjectTagging_non_existing_object, + "PutObjectTagging_long_tags": PutObjectTagging_long_tags, + "PutObjectTagging_success": PutObjectTagging_success, + "GetObjectTagging_non_existing_object": GetObjectTagging_non_existing_object, + "GetObjectTagging_success": GetObjectTagging_success, + "DeleteObjectTagging_non_existing_object": DeleteObjectTagging_non_existing_object, + "DeleteObjectTagging_success_status": DeleteObjectTagging_success_status, + "DeleteObjectTagging_success": DeleteObjectTagging_success, + "CreateMultipartUpload_non_existing_bucket": CreateMultipartUpload_non_existing_bucket, + "CreateMultipartUpload_success": CreateMultipartUpload_success, + "UploadPart_non_existing_bucket": UploadPart_non_existing_bucket, + "UploadPart_invalid_part_number": UploadPart_invalid_part_number, + "UploadPart_non_existing_key": UploadPart_non_existing_key, + "UploadPart_non_existing_mp_upload": UploadPart_non_existing_mp_upload, + "UploadPart_success": UploadPart_success, + "UploadPartCopy_non_existing_bucket": UploadPartCopy_non_existing_bucket, + "UploadPartCopy_incorrect_uploadId": UploadPartCopy_incorrect_uploadId, + "UploadPartCopy_incorrect_object_key": UploadPartCopy_incorrect_object_key, + "UploadPartCopy_invalid_part_number": UploadPartCopy_invalid_part_number, + "UploadPartCopy_invalid_copy_source": UploadPartCopy_invalid_copy_source, + "UploadPartCopy_non_existing_source_bucket": UploadPartCopy_non_existing_source_bucket, + "UploadPartCopy_non_existing_source_object_key": UploadPartCopy_non_existing_source_object_key, + "UploadPartCopy_success": UploadPartCopy_success, + "UploadPartCopy_by_range_invalid_range": UploadPartCopy_by_range_invalid_range, + "UploadPartCopy_greater_range_than_obj_size": UploadPartCopy_greater_range_than_obj_size, + "UploadPartCopy_by_range_success": UploadPartCopy_by_range_success, + "ListParts_incorrect_uploadId": ListParts_incorrect_uploadId, + "ListParts_incorrect_object_key": ListParts_incorrect_object_key, + "ListParts_success": ListParts_success, + "ListMultipartUploads_non_existing_bucket": ListMultipartUploads_non_existing_bucket, + "ListMultipartUploads_empty_result": ListMultipartUploads_empty_result, + "ListMultipartUploads_invalid_max_uploads": ListMultipartUploads_invalid_max_uploads, + "ListMultipartUploads_max_uploads": ListMultipartUploads_max_uploads, + "ListMultipartUploads_incorrect_next_key_marker": ListMultipartUploads_incorrect_next_key_marker, + "ListMultipartUploads_ignore_upload_id_marker": ListMultipartUploads_ignore_upload_id_marker, + "ListMultipartUploads_success": ListMultipartUploads_success, + "AbortMultipartUpload_non_existing_bucket": AbortMultipartUpload_non_existing_bucket, + "AbortMultipartUpload_incorrect_uploadId": AbortMultipartUpload_incorrect_uploadId, + "AbortMultipartUpload_incorrect_object_key": AbortMultipartUpload_incorrect_object_key, + "AbortMultipartUpload_success": AbortMultipartUpload_success, + "AbortMultipartUpload_success_status_code": AbortMultipartUpload_success_status_code, + "CompletedMultipartUpload_non_existing_bucket": CompletedMultipartUpload_non_existing_bucket, + "CompleteMultipartUpload_invalid_part_number": CompleteMultipartUpload_invalid_part_number, + "CompleteMultipartUpload_invalid_ETag": CompleteMultipartUpload_invalid_ETag, + "CompleteMultipartUpload_success": CompleteMultipartUpload_success, + "PutBucketAcl_non_existing_bucket": PutBucketAcl_non_existing_bucket, + "PutBucketAcl_invalid_acl_canned_and_acp": PutBucketAcl_invalid_acl_canned_and_acp, + "PutBucketAcl_invalid_acl_canned_and_grants": PutBucketAcl_invalid_acl_canned_and_grants, + "PutBucketAcl_invalid_acl_acp_and_grants": PutBucketAcl_invalid_acl_acp_and_grants, + "PutBucketAcl_invalid_owner": PutBucketAcl_invalid_owner, + "PutBucketAcl_success_access_denied": PutBucketAcl_success_access_denied, + "PutBucketAcl_success_grants": PutBucketAcl_success_grants, + "PutBucketAcl_success_canned_acl": PutBucketAcl_success_canned_acl, + "PutBucketAcl_success_acp": PutBucketAcl_success_acp, + "GetBucketAcl_non_existing_bucket": GetBucketAcl_non_existing_bucket, + "GetBucketAcl_access_denied": GetBucketAcl_access_denied, + "GetBucketAcl_success": GetBucketAcl_success, + "PutBucketPolicy_non_existing_bucket": PutBucketPolicy_non_existing_bucket, + "PutBucketPolicy_invalid_effect": PutBucketPolicy_invalid_effect, + "PutBucketPolicy_empty_actions_string": PutBucketPolicy_empty_actions_string, + "PutBucketPolicy_empty_actions_array": PutBucketPolicy_empty_actions_array, + "PutBucketPolicy_invalid_action": PutBucketPolicy_invalid_action, + "PutBucketPolicy_unsupported_action": PutBucketPolicy_unsupported_action, + "PutBucketPolicy_incorrect_action_wildcard_usage": PutBucketPolicy_incorrect_action_wildcard_usage, + "PutBucketPolicy_empty_principals_string": PutBucketPolicy_empty_principals_string, + "PutBucketPolicy_empty_principals_array": PutBucketPolicy_empty_principals_array, + "PutBucketPolicy_principals_incorrect_wildcard_usage": PutBucketPolicy_principals_incorrect_wildcard_usage, + "PutBucketPolicy_non_existing_principals": PutBucketPolicy_non_existing_principals, + "PutBucketPolicy_empty_resources_string": PutBucketPolicy_empty_resources_string, + "PutBucketPolicy_empty_resources_array": PutBucketPolicy_empty_resources_array, + "PutBucketPolicy_invalid_resource_prefix": PutBucketPolicy_invalid_resource_prefix, + "PutBucketPolicy_invalid_resource_with_starting_slash": PutBucketPolicy_invalid_resource_with_starting_slash, + "PutBucketPolicy_duplicate_resource": PutBucketPolicy_duplicate_resource, + "PutBucketPolicy_incorrect_bucket_name": PutBucketPolicy_incorrect_bucket_name, + "PutBucketPolicy_object_action_on_bucket_resource": PutBucketPolicy_object_action_on_bucket_resource, + "PutBucketPolicy_bucket_action_on_object_resource": PutBucketPolicy_bucket_action_on_object_resource, + "PutBucketPolicy_success": PutBucketPolicy_success, + "GetBucketPolicy_non_existing_bucket": GetBucketPolicy_non_existing_bucket, + "GetBucketPolicy_default_empty_policy": GetBucketPolicy_default_empty_policy, + "GetBucketPolicy_success": GetBucketPolicy_success, + "DeleteBucketPolicy_non_existing_bucket": DeleteBucketPolicy_non_existing_bucket, + "DeleteBucketPolicy_remove_before_setting": DeleteBucketPolicy_remove_before_setting, + "DeleteBucketPolicy_success": DeleteBucketPolicy_success, + "PutObjectLockConfiguration_non_existing_bucket": PutObjectLockConfiguration_non_existing_bucket, + "PutObjectLockConfiguration_empty_config": PutObjectLockConfiguration_empty_config, + "PutObjectLockConfiguration_both_years_and_days": PutObjectLockConfiguration_both_years_and_days, + "PutObjectLockConfiguration_success": PutObjectLockConfiguration_success, + "GetObjectLockConfiguration_non_existing_bucket": GetObjectLockConfiguration_non_existing_bucket, + "GetObjectLockConfiguration_unset_config": GetObjectLockConfiguration_unset_config, + "GetObjectLockConfiguration_success": GetObjectLockConfiguration_success, + "PutObjectRetention_non_existing_bucket": PutObjectRetention_non_existing_bucket, + "PutObjectRetention_non_existing_object": PutObjectRetention_non_existing_object, + "PutObjectRetention_unset_bucket_object_lock_config": PutObjectRetention_unset_bucket_object_lock_config, + "PutObjectRetention_disabled_bucket_object_lock_config": PutObjectRetention_disabled_bucket_object_lock_config, + "PutObjectRetention_expired_retain_until_date": PutObjectRetention_expired_retain_until_date, + "PutObjectRetention_success": PutObjectRetention_success, + "GetObjectRetention_non_existing_bucket": GetObjectRetention_non_existing_bucket, + "GetObjectRetention_non_existing_object": GetObjectRetention_non_existing_object, + "GetObjectRetention_unset_config": GetObjectRetention_unset_config, + "GetObjectRetention_success": GetObjectRetention_success, + "PutObjectLegalHold_non_existing_bucket": PutObjectLegalHold_non_existing_bucket, + "PutObjectLegalHold_non_existing_object": PutObjectLegalHold_non_existing_object, + "PutObjectLegalHold_invalid_body": PutObjectLegalHold_invalid_body, + "PutObjectLegalHold_unset_bucket_object_lock_config": PutObjectLegalHold_unset_bucket_object_lock_config, + "PutObjectLegalHold_disabled_bucket_object_lock_config": PutObjectLegalHold_disabled_bucket_object_lock_config, + "PutObjectLegalHold_success": PutObjectLegalHold_success, + "GetObjectLegalHold_non_existing_bucket": GetObjectLegalHold_non_existing_bucket, + "GetObjectLegalHold_non_existing_object": GetObjectLegalHold_non_existing_object, + "GetObjectLegalHold_unset_config": GetObjectLegalHold_unset_config, + "GetObjectLegalHold_success": GetObjectLegalHold_success, + "WORMProtection_bucket_object_lock_configuration_compliance_mode": WORMProtection_bucket_object_lock_configuration_compliance_mode, + "WORMProtection_bucket_object_lock_governance_root_overwrite": WORMProtection_bucket_object_lock_governance_root_overwrite, + "WORMProtection_object_lock_retention_compliance_root_access_denied": WORMProtection_object_lock_retention_compliance_root_access_denied, + "WORMProtection_object_lock_retention_governance_root_overwrite": WORMProtection_object_lock_retention_governance_root_overwrite, + "WORMProtection_object_lock_retention_governance_user_access_denied": WORMProtection_object_lock_retention_governance_user_access_denied, + "WORMProtection_object_lock_legal_hold_user_access_denied": WORMProtection_object_lock_legal_hold_user_access_denied, + "WORMProtection_object_lock_legal_hold_root_overwrite": WORMProtection_object_lock_legal_hold_root_overwrite, + "PutObject_overwrite_dir_obj": PutObject_overwrite_dir_obj, + "PutObject_overwrite_file_obj": PutObject_overwrite_file_obj, + "PutObject_dir_obj_with_data": PutObject_dir_obj_with_data, + "CreateMultipartUpload_dir_obj": CreateMultipartUpload_dir_obj, + "IAM_user_access_denied": IAM_user_access_denied, + "IAM_userplus_access_denied": IAM_userplus_access_denied, + "IAM_userplus_CreateBucket": IAM_userplus_CreateBucket, + "IAM_admin_ChangeBucketOwner": IAM_admin_ChangeBucketOwner, } } diff --git a/tests/integration/tests.go b/tests/integration/tests.go index f1b94109..0b54add9 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -5805,6 +5805,1017 @@ func DeleteBucketPolicy_success(s *S3Conf) error { }) } +// Object lock tests +func PutObjectLockConfiguration_non_existing_bucket(s *S3Conf) error { + testName := "PutObjectLockConfiguration_non_existing_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: getPtr(getBucketName()), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectLockConfiguration_empty_config(s *S3Conf) error { + testName := "PutObjectLockConfiguration_empty_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidRequest)); err != nil { + return err + } + return nil + }) +} + +func PutObjectLockConfiguration_both_years_and_days(s *S3Conf) error { + testName := "PutObjectLockConfiguration_both_years_and_days" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + var days, years int32 = 12, 24 + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Days: &days, + Years: &years, + }, + }, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidRequest)); err != nil { + return err + } + return nil + }) +} + +func PutObjectLockConfiguration_success(s *S3Conf) error { + testName := "PutObjectLockConfiguration_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + }, + }) + cancel() + if err != nil { + return err + } + return nil + }) +} + +func GetObjectLockConfiguration_non_existing_bucket(s *S3Conf) error { + testName := "GetObjectLockConfiguration_non_existing_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: getPtr(getBucketName()), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectLockConfiguration_unset_config(s *S3Conf) error { + testName := "GetObjectLockConfiguration_unset_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: &bucket, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectLockConfiguration_success(s *S3Conf) error { + testName := "GetObjectLockConfiguration_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + var days int32 = 20 + config := types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + Days: &days, + }, + }, + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &config, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: &bucket, + }) + cancel() + if err != nil { + return err + } + + if resp.ObjectLockConfiguration == nil { + return fmt.Errorf("got nil object lock configuration") + } + + respConfig := resp.ObjectLockConfiguration + + if respConfig.ObjectLockEnabled != config.ObjectLockEnabled { + return fmt.Errorf("expected lock status to be %v, instead got %v", config.ObjectLockEnabled, respConfig.ObjectLockEnabled) + } + if *respConfig.Rule.DefaultRetention.Days != *config.Rule.DefaultRetention.Days { + return fmt.Errorf("expected lock config days to be %v, instead got %v", *config.Rule.DefaultRetention.Days, *respConfig.Rule.DefaultRetention.Days) + } + if respConfig.Rule.DefaultRetention.Mode != config.Rule.DefaultRetention.Mode { + return fmt.Errorf("expected lock config mode to be %v, instead got %v", config.Rule.DefaultRetention.Mode, respConfig.Rule.DefaultRetention.Mode) + } + + return nil + }) +} + +func PutObjectRetention_non_existing_bucket(s *S3Conf) error { + testName := "PutObjectRetention_non_existing_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: getPtr(getBucketName()), + Key: getPtr("my-obj"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectRetention_non_existing_object(s *S3Conf) error { + testName := "PutObjectRetention_non_existing_object" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + date := time.Now().Add(time.Hour * 3) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &date, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectRetention_unset_bucket_object_lock_config(s *S3Conf) error { + testName := "PutObjectRetention_unset_bucket_object_lock_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + date := time.Now().Add(time.Hour * 3) + key := "my-obj" + + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &key, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &date, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectRetention_disabled_bucket_object_lock_config(s *S3Conf) error { + testName := "PutObjectRetention_disabled_bucket_object_lock_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{}, + }) + cancel() + if err != nil { + return err + } + + date := time.Now().Add(time.Hour * 3) + key := "my-obj" + + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &key, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &date, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectRetention_expired_retain_until_date(s *S3Conf) error { + testName := "PutObjectRetention_expired_retain_until_date" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + date := time.Now().Add(-time.Hour * 3) + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &date, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectRetention_success(s *S3Conf) error { + testName := "PutObjectRetention_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + date := time.Now().Add(time.Hour * 3) + key := "my-obj" + + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &key, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &date, + }, + }) + cancel() + if err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func GetObjectRetention_non_existing_bucket(s *S3Conf) error { + testName := "GetObjectRetention_non_existing_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: getPtr(getBucketName()), + Key: getPtr("my-obj"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectRetention_non_existing_object(s *S3Conf) error { + testName := "GetObjectRetention_non_existing_object" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectRetention_unset_config(s *S3Conf) error { + testName := "GetObjectRetention_unset_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + key := "my-obj" + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: &key, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectRetention_success(s *S3Conf) error { + testName := "GetObjectRetention_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + key := "my-obj" + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + date := time.Now().Add(time.Hour * 3) + retention := types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &date, + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &key, + Retention: &retention, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: &key, + }) + cancel() + if err != nil { + return err + } + + if resp.Retention == nil { + return fmt.Errorf("got nil object lock retention") + } + + ret := resp.Retention + + if ret.Mode != retention.Mode { + return fmt.Errorf("expected retention mode to be %v, instead got %v", retention.Mode, ret.Mode) + } + // FIXME: There's a problem with storing retainUnitDate, most probably SDK changes the date before sending + // if ret.RetainUntilDate.Format(iso8601Format)[:8] != retention.RetainUntilDate.Format(iso8601Format)[:8] { + // return fmt.Errorf("expected retain until date to be %v, instead got %v", retention.RetainUntilDate.Format(iso8601Format), ret.RetainUntilDate.Format(iso8601Format)) + // } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func PutObjectLegalHold_non_existing_bucket(s *S3Conf) error { + testName := "PutObjectLegalHold_non_existing_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: getPtr(getBucketName()), + Key: getPtr("my-obj"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectLegalHold_non_existing_object(s *S3Conf) error { + testName := "PutObjectLegalHold_non_existing_object" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectLegalHold_invalid_body(s *S3Conf) error { + testName := "PutObjectLegalHold_invalid_body" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidRequest)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectLegalHold_unset_bucket_object_lock_config(s *S3Conf) error { + testName := "PutObjectLegalHold_unset_bucket_object_lock_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + key := "my-obj" + + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &key, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectLegalHold_disabled_bucket_object_lock_config(s *S3Conf) error { + testName := "PutObjectLegalHold_disabled_bucket_object_lock_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{}, + }) + cancel() + if err != nil { + return err + } + + key := "my-obj" + + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &key, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration)); err != nil { + return err + } + + return nil + }) +} + +func PutObjectLegalHold_success(s *S3Conf) error { + testName := "PutObjectLegalHold_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + key := "my-obj" + + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &key, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func GetObjectLegalHold_non_existing_bucket(s *S3Conf) error { + testName := "GetObjectLegalHold_non_existing_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: getPtr(getBucketName()), + Key: getPtr("my-obj"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectLegalHold_non_existing_object(s *S3Conf) error { + testName := "GetObjectLegalHold_non_existing_object" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectLegalHold_unset_config(s *S3Conf) error { + testName := "GetObjectLegalHold_unset_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + key := "my-obj" + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: &key, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectLegalHold_success(s *S3Conf) error { + testName := "GetObjectLegalHold_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + key := "my-obj" + if err := putObjects(s3client, []string{key}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &key, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: &key, + }) + cancel() + if err != nil { + return err + } + + if resp.LegalHold.Status != types.ObjectLockLegalHoldStatusOn { + return fmt.Errorf("expected legal hold status to be On, instead got %v", resp.LegalHold.Status) + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func WORMProtection_bucket_object_lock_configuration_compliance_mode(s *S3Conf) error { + testName := "WORMProtection_bucket_object_lock_configuration_compliance_mode" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + var days int32 = 10 + object := "my-obj" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + Days: &days, + }, + }, + }, + }) + cancel() + if err != nil { + return err + } + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + if err := checkWORMProtection(s3client, bucket, object); err != nil { + return err + } + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func WORMProtection_bucket_object_lock_governance_root_overwrite(s *S3Conf) error { + testName := "WORMProtection_bucket_object_lock_governance_root_overwrite" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + var days int32 = 10 + object := "my-obj" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + Days: &days, + }, + }, + }, + }) + cancel() + if err != nil { + return err + } + + // create an object + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + // overwrite the object + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func WORMProtection_object_lock_retention_compliance_root_access_denied(s *S3Conf) error { + testName := "WORMProtection_object_lock_retention_compliance_root_access_denied" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + object := "my-obj" + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + date := time.Now().Add(time.Hour * 3) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &object, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &date, + }, + }) + cancel() + if err != nil { + return err + } + + if err := checkWORMProtection(s3client, bucket, object); err != nil { + return err + } + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func WORMProtection_object_lock_retention_governance_root_overwrite(s *S3Conf) error { + testName := "WORMProtection_object_lock_retention_governance_root_overwrite" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + object := "my-obj" + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + date := time.Now().Add(time.Hour * 3) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &object, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + RetainUntilDate: &date, + }, + }) + cancel() + if err != nil { + return err + } + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func WORMProtection_object_lock_retention_governance_user_access_denied(s *S3Conf) error { + testName := "WORMProtection_object_lock_retention_governance_user_access_denied" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + object := "my-obj" + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + date := time.Now().Add(time.Hour * 3) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &object, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + RetainUntilDate: &date, + }, + }) + cancel() + if err != nil { + return err + } + + usr := user{ + access: "grt1", + secret: "grt1secret", + role: "user", + } + if err := createUsers(s, []user{usr}); err != nil { + return err + } + if err := changeBucketsOwner(s, []string{bucket}, usr.access); err != nil { + return err + } + + cfg := *s + cfg.awsID = usr.access + cfg.awsSecret = usr.secret + + err = putObjects(s3.NewFromConfig(cfg.Config()), []string{object}, bucket) + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func WORMProtection_object_lock_legal_hold_user_access_denied(s *S3Conf) error { + testName := "WORMProtection_object_lock_legal_hold_user_access_denied" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + object := "my-obj" + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &object, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err != nil { + return err + } + + usr := user{ + access: "grt1", + secret: "grt1secret", + role: "user", + } + if err := createUsers(s, []user{usr}); err != nil { + return err + } + if err := changeBucketsOwner(s, []string{bucket}, usr.access); err != nil { + return err + } + + cfg := *s + cfg.awsID = usr.access + cfg.awsSecret = usr.secret + + err = putObjects(s3.NewFromConfig(cfg.Config()), []string{object}, bucket) + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + +func WORMProtection_object_lock_legal_hold_root_overwrite(s *S3Conf) error { + testName := "WORMProtection_object_lock_legal_hold_root_overwrite" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { + return err + } + + object := "my-obj" + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &object, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err != nil { + return err + } + + if err := putObjects(s3client, []string{object}, bucket); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }) +} + // Access control tests (with bucket ACLs and Policies) func AccessControl_default_ACL_user_access_denied(s *S3Conf) error { testName := "AccessControl_default_ACL_user_access_denied" diff --git a/tests/integration/utils.go b/tests/integration/utils.go index bd13800a..71305588 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -645,3 +645,62 @@ func getUserS3Client(usr user, cfg *S3Conf) *s3.Client { return s3.NewFromConfig(config.Config()) } + +// if true enables, otherwise disables +func changeBucketObjectLockStatus(client *s3.Client, bucket string, status bool) error { + cfg := types.ObjectLockConfiguration{} + if status { + cfg.ObjectLockEnabled = types.ObjectLockEnabledEnabled + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &cfg, + }) + cancel() + if err != nil { + return err + } + + return nil +} + +func checkWORMProtection(client *s3.Client, bucket, object string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &object, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &object, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: &bucket, + Delete: &types.Delete{ + Objects: []types.ObjectIdentifier{ + { + Key: &object, + }, + }, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + return nil +} From 89755ea5aa94f3828fcdc9d8aa12c804d5e35529 Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Mon, 22 Apr 2024 15:01:09 -0400 Subject: [PATCH 3/3] feat: Changed object lock actions interface to put/get []byte --- auth/object_lock.go | 137 +++++++++++++++++++----- backend/backend.go | 21 ++-- backend/posix/posix.go | 147 +++++--------------------- s3api/controllers/backend_moq_test.go | 139 +++++++++++++++--------- s3api/controllers/base.go | 82 ++++++-------- s3api/controllers/base_test.go | 21 ++-- 6 files changed, 281 insertions(+), 266 deletions(-) diff --git a/auth/object_lock.go b/auth/object_lock.go index 4a1f1b21..6a5fbcbe 100644 --- a/auth/object_lock.go +++ b/auth/object_lock.go @@ -17,6 +17,7 @@ package auth import ( "context" "encoding/json" + "encoding/xml" "errors" "fmt" "time" @@ -37,6 +38,30 @@ type ObjectLockConfig struct { Retention *types.ObjectLockRetention } +func ParseBucketLockConfigurationInput(input []byte) ([]byte, error) { + var lockConfig types.ObjectLockConfiguration + if err := xml.Unmarshal(input, &lockConfig); err != nil { + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + config := BucketLockConfig{ + Enabled: lockConfig.ObjectLockEnabled == types.ObjectLockEnabledEnabled, + } + + if lockConfig.Rule != nil && lockConfig.Rule.DefaultRetention != nil { + retention := lockConfig.Rule.DefaultRetention + if retention.Years != nil && retention.Days != nil { + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + config.DefaultRetention = retention + now := time.Now() + config.CreatedAt = &now + } + + return json.Marshal(config) +} + func ParseBucketLockConfigurationOutput(input []byte) (*types.ObjectLockConfiguration, error) { var config BucketLockConfig if err := json.Unmarshal(input, &config); err != nil { @@ -56,6 +81,50 @@ func ParseBucketLockConfigurationOutput(input []byte) (*types.ObjectLockConfigur return result, nil } +func ParseObjectLockRetentionInput(input []byte) ([]byte, error) { + var retention types.ObjectLockRetention + if err := xml.Unmarshal(input, &retention); err != nil { + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + if retention.RetainUntilDate == nil || retention.RetainUntilDate.Before(time.Now()) { + return nil, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate) + } + switch retention.Mode { + case types.ObjectLockRetentionModeCompliance: + case types.ObjectLockRetentionModeGovernance: + default: + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + return json.Marshal(retention) +} + +func ParseObjectLockRetentionOutput(input []byte) (*types.ObjectLockRetention, error) { + var retention types.ObjectLockRetention + if err := json.Unmarshal(input, &retention); err != nil { + return nil, fmt.Errorf("parse object lock retention: %w", err) + } + + return &retention, nil +} + +func ParseObjectLegalHoldOutput(status *bool) *types.ObjectLockLegalHold { + if status == nil { + return nil + } + + if *status { + return &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + } + } + + return &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOff, + } +} + func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []string, isAdminOrRoot bool, be backend.Backend) error { data, err := be.GetObjectLockConfiguration(ctx, bucket) if err != nil { @@ -78,48 +147,58 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ objExists := true for _, obj := range objects { - retention, err := be.GetObjectRetention(ctx, bucket, obj, "") - if err != nil { - if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { - objExists = false - continue - } - if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) { - continue - } - + var checkRetention bool = true + retentionData, err := be.GetObjectRetention(ctx, bucket, obj, "") + if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { + objExists = false + continue + } + if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) { + checkRetention = false + } + if err != nil && checkRetention { return err } - if retention.Mode != "" && retention.RetainUntilDate != nil { - if retention.RetainUntilDate.After(time.Now()) { - switch retention.Mode { - case types.ObjectLockRetentionModeGovernance: - if !isAdminOrRoot { - policy, err := be.GetBucketPolicy(ctx, bucket) - if err != nil { - return err - } - if len(policy) == 0 { - return s3err.GetAPIError(s3err.ErrObjectLocked) - } - err = verifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) - if err != nil { - return s3err.GetAPIError(s3err.ErrObjectLocked) + if checkRetention { + retention, err := ParseObjectLockRetentionOutput(retentionData) + if err != nil { + return err + } + + if retention.Mode != "" && retention.RetainUntilDate != nil { + if retention.RetainUntilDate.After(time.Now()) { + switch retention.Mode { + case types.ObjectLockRetentionModeGovernance: + if !isAdminOrRoot { + policy, err := be.GetBucketPolicy(ctx, bucket) + if err != nil { + return err + } + if len(policy) == 0 { + return s3err.GetAPIError(s3err.ErrObjectLocked) + } + err = verifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) + if err != nil { + return s3err.GetAPIError(s3err.ErrObjectLocked) + } } + case types.ObjectLockRetentionModeCompliance: + return s3err.GetAPIError(s3err.ErrObjectLocked) } - case types.ObjectLockRetentionModeCompliance: - return s3err.GetAPIError(s3err.ErrObjectLocked) } } } - legalHold, err := be.GetObjectLegalHold(ctx, bucket, obj, "") + status, err := be.GetObjectLegalHold(ctx, bucket, obj, "") + if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) { + continue + } if err != nil { return err } - if legalHold.Status == types.ObjectLockLegalHoldStatusOn && !isAdminOrRoot { + if *status && !isAdminOrRoot { return s3err.GetAPIError(s3err.ErrObjectLocked) } } diff --git a/backend/backend.go b/backend/backend.go index a0a79d17..fa060d58 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -21,7 +21,6 @@ import ( "io" "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3response" "github.com/versity/versitygw/s3select" @@ -83,12 +82,12 @@ type Backend interface { DeleteObjectTagging(_ context.Context, bucket, object string) error // object lock operations - PutObjectLockConfiguration(context.Context, *s3.PutObjectLockConfigurationInput) error + PutObjectLockConfiguration(_ context.Context, bucket string, config []byte) error GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) - PutObjectRetention(context.Context, *s3.PutObjectRetentionInput) error - GetObjectRetention(_ context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) - PutObjectLegalHold(context.Context, *s3.PutObjectLegalHoldInput) error - GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) + PutObjectRetention(_ context.Context, bucket, object, versionId string, retention []byte) error + GetObjectRetention(_ context.Context, bucket, object, versionId string) ([]byte, error) + PutObjectLegalHold(_ context.Context, bucket, object, versionId string, status bool) error + GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*bool, error) // non AWS actions ChangeBucketOwner(_ context.Context, bucket, newOwner string) error @@ -238,22 +237,22 @@ func (BackendUnsupported) DeleteObjectTagging(_ context.Context, bucket, object return s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) PutObjectLockConfiguration(context.Context, *s3.PutObjectLockConfigurationInput) error { +func (BackendUnsupported) PutObjectLockConfiguration(_ context.Context, bucket string, config []byte) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } func (BackendUnsupported) GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) PutObjectRetention(context.Context, *s3.PutObjectRetentionInput) error { +func (BackendUnsupported) PutObjectRetention(_ context.Context, bucket, object, versionId string, retention []byte) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) GetObjectRetention(_ context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) { +func (BackendUnsupported) GetObjectRetention(_ context.Context, bucket, object, versionId string) ([]byte, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) PutObjectLegalHold(context.Context, *s3.PutObjectLegalHoldInput) error { +func (BackendUnsupported) PutObjectLegalHold(_ context.Context, bucket, object, versionId string, status bool) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) { +func (BackendUnsupported) GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*bool, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) } diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 7ce5abb9..fd17035c 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -30,12 +30,10 @@ import ( "strconv" "strings" "syscall" - "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/google/uuid" - "github.com/pkg/xattr" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/backend/meta" @@ -78,7 +76,8 @@ const ( etagkey = "etag" policykey = "policy" bucketLockKey = "bucket-lock" - objectLockKey = "object-lock" + objectRetentionKey = "object-retention" + objectLegalHoldKey = "object-legal-hold" ) type PosixOpts struct { @@ -2015,8 +2014,8 @@ func (p *Posix) DeleteBucketPolicy(ctx context.Context, bucket string) error { return p.PutBucketPolicy(ctx, bucket, nil) } -func (p *Posix) PutObjectLockConfiguration(_ context.Context, input *s3.PutObjectLockConfigurationInput) error { - _, err := os.Stat(*input.Bucket) +func (p *Posix) PutObjectLockConfiguration(_ context.Context, bucket string, config []byte) error { + _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) } @@ -2024,30 +2023,8 @@ func (p *Posix) PutObjectLockConfiguration(_ context.Context, input *s3.PutObjec return fmt.Errorf("stat bucket: %w", err) } - lockConfig := input.ObjectLockConfiguration - - config := auth.BucketLockConfig{ - Enabled: lockConfig.ObjectLockEnabled == types.ObjectLockEnabledEnabled, - } - - if lockConfig.Rule != nil && lockConfig.Rule.DefaultRetention != nil { - retentation := lockConfig.Rule.DefaultRetention - if retentation.Years != nil && retentation.Days != nil { - return s3err.GetAPIError(s3err.ErrInvalidRequest) - } - - config.DefaultRetention = retentation - now := time.Now() - config.CreatedAt = &now - } - - configParsed, err := json.Marshal(config) - if err != nil { - return fmt.Errorf("parse object lock config: %w", err) - } - - if err := xattr.Set(*input.Bucket, bucketLockKey, configParsed); err != nil { - return fmt.Errorf("set tags: %w", err) + if err := p.meta.StoreAttribute(bucket, "", bucketLockKey, config); err != nil { + return fmt.Errorf("set object lock config: %w", err) } return nil @@ -2062,7 +2039,7 @@ func (p *Posix) GetObjectLockConfiguration(_ context.Context, bucket string) ([] return nil, fmt.Errorf("stat bucket: %w", err) } - cfg, err := xattr.Get(bucket, bucketLockKey) + cfg, err := p.meta.RetrieveAttribute(bucket, "", bucketLockKey) if errors.Is(err, meta.ErrNoSuchKey) { return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) } @@ -2073,8 +2050,8 @@ func (p *Posix) GetObjectLockConfiguration(_ context.Context, bucket string) ([] return cfg, nil } -func (p *Posix) PutObjectLegalHold(_ context.Context, input *s3.PutObjectLegalHoldInput) error { - _, err := os.Stat(*input.Bucket) +func (p *Posix) PutObjectLegalHold(_ context.Context, bucket, object, versionId string, status bool) error { + _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) } @@ -2082,7 +2059,7 @@ func (p *Posix) PutObjectLegalHold(_ context.Context, input *s3.PutObjectLegalHo return fmt.Errorf("stat bucket: %w", err) } - cfg, err := xattr.Get(*input.Bucket, bucketLockKey) + cfg, err := p.meta.RetrieveAttribute(bucket, "", bucketLockKey) if errors.Is(err, meta.ErrNoSuchKey) { return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) } @@ -2099,40 +2076,14 @@ func (p *Posix) PutObjectLegalHold(_ context.Context, input *s3.PutObjectLegalHo return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) } - path := filepath.Join(*input.Bucket, *input.Key) - var config auth.ObjectLockConfig - - data, err := xattr.Get(path, objectLockKey) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchKey) - } - if errors.Is(err, meta.ErrNoSuchKey) { - return fmt.Errorf("get object lock config: %w", err) - } - - config = auth.ObjectLockConfig{} + var statusData []byte + if status { + statusData = []byte{1} } else { - if err := json.Unmarshal(data, &config); err != nil { - return fmt.Errorf("parse object lock data %w", err) - } + statusData = []byte{0} } - switch input.LegalHold.Status { - case types.ObjectLockLegalHoldStatusOff: - config.LegalHoldEnabled = false - case types.ObjectLockLegalHoldStatusOn: - config.LegalHoldEnabled = true - default: - return s3err.GetAPIError(s3err.ErrInvalidRequest) - } - - b, err := json.Marshal(config) - if err != nil { - return fmt.Errorf("marshal object lock config: %w", err) - } - - err = xattr.Set(path, objectLockKey, b) + err = p.meta.StoreAttribute(bucket, object, objectLegalHoldKey, statusData) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -2143,7 +2094,7 @@ func (p *Posix) PutObjectLegalHold(_ context.Context, input *s3.PutObjectLegalHo return nil } -func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) { +func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*bool, error) { _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) @@ -2152,7 +2103,7 @@ func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId return nil, fmt.Errorf("stat bucket: %w", err) } - data, err := xattr.Get(filepath.Join(bucket, object), objectLockKey) + data, err := p.meta.RetrieveAttribute(bucket, object, objectLegalHoldKey) if errors.Is(err, fs.ErrNotExist) { return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -2163,24 +2114,13 @@ func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId return nil, fmt.Errorf("get object lock config: %w", err) } - var config auth.ObjectLockConfig - if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("pare object lock config: %w", err) - } + result := data[0] == 1 - result := &types.ObjectLockLegalHold{} - - if config.LegalHoldEnabled { - result.Status = types.ObjectLockLegalHoldStatusOn - } else { - result.Status = types.ObjectLockLegalHoldStatusOff - } - - return result, nil + return &result, nil } -func (p *Posix) PutObjectRetention(_ context.Context, input *s3.PutObjectRetentionInput) error { - _, err := os.Stat(*input.Bucket) +func (p *Posix) PutObjectRetention(_ context.Context, bucket, object, versionId string, retention []byte) error { + _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) } @@ -2188,7 +2128,7 @@ func (p *Posix) PutObjectRetention(_ context.Context, input *s3.PutObjectRetenti return fmt.Errorf("stat bucket: %w", err) } - cfg, err := xattr.Get(*input.Bucket, bucketLockKey) + cfg, err := p.meta.RetrieveAttribute(bucket, "", bucketLockKey) if errors.Is(err, meta.ErrNoSuchKey) { return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) } @@ -2205,33 +2145,7 @@ func (p *Posix) PutObjectRetention(_ context.Context, input *s3.PutObjectRetenti return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) } - path := filepath.Join(*input.Bucket, *input.Key) - var config auth.ObjectLockConfig - - data, err := xattr.Get(path, objectLockKey) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchKey) - } - if errors.Is(err, meta.ErrNoSuchKey) { - return fmt.Errorf("get object lock config: %w", err) - } - - config = auth.ObjectLockConfig{} - } else { - if err := json.Unmarshal(data, &config); err != nil { - return fmt.Errorf("parse object lock data %w", err) - } - } - - config.Retention = input.Retention - - b, err := json.Marshal(config) - if err != nil { - return fmt.Errorf("marshal object lock config: %w", err) - } - - err = xattr.Set(path, objectLockKey, b) + err = p.meta.StoreAttribute(bucket, object, objectRetentionKey, retention) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -2242,7 +2156,7 @@ func (p *Posix) PutObjectRetention(_ context.Context, input *s3.PutObjectRetenti return nil } -func (p *Posix) GetObjectRetention(_ context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) { +func (p *Posix) GetObjectRetention(_ context.Context, bucket, object, versionId string) ([]byte, error) { _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) @@ -2251,7 +2165,7 @@ func (p *Posix) GetObjectRetention(_ context.Context, bucket, object, versionId return nil, fmt.Errorf("stat bucket: %w", err) } - data, err := xattr.Get(filepath.Join(bucket, object), objectLockKey) + data, err := p.meta.RetrieveAttribute(bucket, object, objectRetentionKey) if errors.Is(err, fs.ErrNotExist) { return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -2262,16 +2176,7 @@ func (p *Posix) GetObjectRetention(_ context.Context, bucket, object, versionId return nil, fmt.Errorf("get object lock config: %w", err) } - var config auth.ObjectLockConfig - if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("pare object lock config: %w", err) - } - - if config.Retention == nil { - return &types.ObjectLockRetention{}, nil - } - - return config.Retention, nil + return data, nil } func (p *Posix) ChangeBucketOwner(ctx context.Context, bucket, newOwner string) error { diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index f56e6ce3..b943fede 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -7,7 +7,6 @@ import ( "bufio" "context" "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/s3response" "io" @@ -81,13 +80,13 @@ var _ backend.Backend = &BackendMock{} // GetObjectAttributesFunc: func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { // panic("mock out the GetObjectAttributes method") // }, -// GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockLegalHold, error) { +// GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) (*bool, error) { // panic("mock out the GetObjectLegalHold method") // }, // GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { // panic("mock out the GetObjectLockConfiguration method") // }, -// GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockRetention, error) { +// GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) ([]byte, error) { // panic("mock out the GetObjectRetention method") // }, // GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) { @@ -138,13 +137,13 @@ var _ backend.Backend = &BackendMock{} // PutObjectAclFunc: func(contextMoqParam context.Context, putObjectAclInput *s3.PutObjectAclInput) error { // panic("mock out the PutObjectAcl method") // }, -// PutObjectLegalHoldFunc: func(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error { +// PutObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string, status bool) error { // panic("mock out the PutObjectLegalHold method") // }, -// PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error { +// PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string, config []byte) error { // panic("mock out the PutObjectLockConfiguration method") // }, -// PutObjectRetentionFunc: func(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error { +// PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error { // panic("mock out the PutObjectRetention method") // }, // PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { @@ -233,13 +232,13 @@ type BackendMock struct { GetObjectAttributesFunc func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) // GetObjectLegalHoldFunc mocks the GetObjectLegalHold method. - GetObjectLegalHoldFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockLegalHold, error) + GetObjectLegalHoldFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) (*bool, error) // GetObjectLockConfigurationFunc mocks the GetObjectLockConfiguration method. GetObjectLockConfigurationFunc func(contextMoqParam context.Context, bucket string) ([]byte, error) // GetObjectRetentionFunc mocks the GetObjectRetention method. - GetObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockRetention, error) + GetObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) ([]byte, error) // GetObjectTaggingFunc mocks the GetObjectTagging method. GetObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) @@ -290,13 +289,13 @@ type BackendMock struct { PutObjectAclFunc func(contextMoqParam context.Context, putObjectAclInput *s3.PutObjectAclInput) error // PutObjectLegalHoldFunc mocks the PutObjectLegalHold method. - PutObjectLegalHoldFunc func(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error + PutObjectLegalHoldFunc func(contextMoqParam context.Context, bucket string, object string, versionId string, status bool) error // PutObjectLockConfigurationFunc mocks the PutObjectLockConfiguration method. - PutObjectLockConfigurationFunc func(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error + PutObjectLockConfigurationFunc func(contextMoqParam context.Context, bucket string, config []byte) error // PutObjectRetentionFunc mocks the PutObjectRetention method. - PutObjectRetentionFunc func(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error + PutObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error // PutObjectTaggingFunc mocks the PutObjectTagging method. PutObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error @@ -615,22 +614,36 @@ type BackendMock struct { PutObjectLegalHold []struct { // ContextMoqParam is the contextMoqParam argument value. ContextMoqParam context.Context - // PutObjectLegalHoldInput is the putObjectLegalHoldInput argument value. - PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput + // Bucket is the bucket argument value. + Bucket string + // Object is the object argument value. + Object string + // VersionId is the versionId argument value. + VersionId string + // Status is the status argument value. + Status bool } // PutObjectLockConfiguration holds details about calls to the PutObjectLockConfiguration method. PutObjectLockConfiguration []struct { // ContextMoqParam is the contextMoqParam argument value. ContextMoqParam context.Context - // PutObjectLockConfigurationInput is the putObjectLockConfigurationInput argument value. - PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput + // Bucket is the bucket argument value. + Bucket string + // Config is the config argument value. + Config []byte } // PutObjectRetention holds details about calls to the PutObjectRetention method. PutObjectRetention []struct { // ContextMoqParam is the contextMoqParam argument value. ContextMoqParam context.Context - // PutObjectRetentionInput is the putObjectRetentionInput argument value. - PutObjectRetentionInput *s3.PutObjectRetentionInput + // Bucket is the bucket argument value. + Bucket string + // Object is the object argument value. + Object string + // VersionId is the versionId argument value. + VersionId string + // Retention is the retention argument value. + Retention []byte } // PutObjectTagging holds details about calls to the PutObjectTagging method. PutObjectTagging []struct { @@ -1429,7 +1442,7 @@ func (mock *BackendMock) GetObjectAttributesCalls() []struct { } // GetObjectLegalHold calls GetObjectLegalHoldFunc. -func (mock *BackendMock) GetObjectLegalHold(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockLegalHold, error) { +func (mock *BackendMock) GetObjectLegalHold(contextMoqParam context.Context, bucket string, object string, versionId string) (*bool, error) { if mock.GetObjectLegalHoldFunc == nil { panic("BackendMock.GetObjectLegalHoldFunc: method is nil but Backend.GetObjectLegalHold was just called") } @@ -1509,7 +1522,7 @@ func (mock *BackendMock) GetObjectLockConfigurationCalls() []struct { } // GetObjectRetention calls GetObjectRetentionFunc. -func (mock *BackendMock) GetObjectRetention(contextMoqParam context.Context, bucket string, object string, versionId string) (*types.ObjectLockRetention, error) { +func (mock *BackendMock) GetObjectRetention(contextMoqParam context.Context, bucket string, object string, versionId string) ([]byte, error) { if mock.GetObjectRetentionFunc == nil { panic("BackendMock.GetObjectRetentionFunc: method is nil but Backend.GetObjectRetention was just called") } @@ -2145,21 +2158,27 @@ func (mock *BackendMock) PutObjectAclCalls() []struct { } // PutObjectLegalHold calls PutObjectLegalHoldFunc. -func (mock *BackendMock) PutObjectLegalHold(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error { +func (mock *BackendMock) PutObjectLegalHold(contextMoqParam context.Context, bucket string, object string, versionId string, status bool) error { if mock.PutObjectLegalHoldFunc == nil { panic("BackendMock.PutObjectLegalHoldFunc: method is nil but Backend.PutObjectLegalHold was just called") } callInfo := struct { - ContextMoqParam context.Context - PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + Status bool }{ - ContextMoqParam: contextMoqParam, - PutObjectLegalHoldInput: putObjectLegalHoldInput, + ContextMoqParam: contextMoqParam, + Bucket: bucket, + Object: object, + VersionId: versionId, + Status: status, } mock.lockPutObjectLegalHold.Lock() mock.calls.PutObjectLegalHold = append(mock.calls.PutObjectLegalHold, callInfo) mock.lockPutObjectLegalHold.Unlock() - return mock.PutObjectLegalHoldFunc(contextMoqParam, putObjectLegalHoldInput) + return mock.PutObjectLegalHoldFunc(contextMoqParam, bucket, object, versionId, status) } // PutObjectLegalHoldCalls gets all the calls that were made to PutObjectLegalHold. @@ -2167,12 +2186,18 @@ func (mock *BackendMock) PutObjectLegalHold(contextMoqParam context.Context, put // // len(mockedBackend.PutObjectLegalHoldCalls()) func (mock *BackendMock) PutObjectLegalHoldCalls() []struct { - ContextMoqParam context.Context - PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + Status bool } { var calls []struct { - ContextMoqParam context.Context - PutObjectLegalHoldInput *s3.PutObjectLegalHoldInput + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + Status bool } mock.lockPutObjectLegalHold.RLock() calls = mock.calls.PutObjectLegalHold @@ -2181,21 +2206,23 @@ func (mock *BackendMock) PutObjectLegalHoldCalls() []struct { } // PutObjectLockConfiguration calls PutObjectLockConfigurationFunc. -func (mock *BackendMock) PutObjectLockConfiguration(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error { +func (mock *BackendMock) PutObjectLockConfiguration(contextMoqParam context.Context, bucket string, config []byte) error { if mock.PutObjectLockConfigurationFunc == nil { panic("BackendMock.PutObjectLockConfigurationFunc: method is nil but Backend.PutObjectLockConfiguration was just called") } callInfo := struct { - ContextMoqParam context.Context - PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput + ContextMoqParam context.Context + Bucket string + Config []byte }{ - ContextMoqParam: contextMoqParam, - PutObjectLockConfigurationInput: putObjectLockConfigurationInput, + ContextMoqParam: contextMoqParam, + Bucket: bucket, + Config: config, } mock.lockPutObjectLockConfiguration.Lock() mock.calls.PutObjectLockConfiguration = append(mock.calls.PutObjectLockConfiguration, callInfo) mock.lockPutObjectLockConfiguration.Unlock() - return mock.PutObjectLockConfigurationFunc(contextMoqParam, putObjectLockConfigurationInput) + return mock.PutObjectLockConfigurationFunc(contextMoqParam, bucket, config) } // PutObjectLockConfigurationCalls gets all the calls that were made to PutObjectLockConfiguration. @@ -2203,12 +2230,14 @@ func (mock *BackendMock) PutObjectLockConfiguration(contextMoqParam context.Cont // // len(mockedBackend.PutObjectLockConfigurationCalls()) func (mock *BackendMock) PutObjectLockConfigurationCalls() []struct { - ContextMoqParam context.Context - PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput + ContextMoqParam context.Context + Bucket string + Config []byte } { var calls []struct { - ContextMoqParam context.Context - PutObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput + ContextMoqParam context.Context + Bucket string + Config []byte } mock.lockPutObjectLockConfiguration.RLock() calls = mock.calls.PutObjectLockConfiguration @@ -2217,21 +2246,27 @@ func (mock *BackendMock) PutObjectLockConfigurationCalls() []struct { } // PutObjectRetention calls PutObjectRetentionFunc. -func (mock *BackendMock) PutObjectRetention(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error { +func (mock *BackendMock) PutObjectRetention(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error { if mock.PutObjectRetentionFunc == nil { panic("BackendMock.PutObjectRetentionFunc: method is nil but Backend.PutObjectRetention was just called") } callInfo := struct { - ContextMoqParam context.Context - PutObjectRetentionInput *s3.PutObjectRetentionInput + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + Retention []byte }{ - ContextMoqParam: contextMoqParam, - PutObjectRetentionInput: putObjectRetentionInput, + ContextMoqParam: contextMoqParam, + Bucket: bucket, + Object: object, + VersionId: versionId, + Retention: retention, } mock.lockPutObjectRetention.Lock() mock.calls.PutObjectRetention = append(mock.calls.PutObjectRetention, callInfo) mock.lockPutObjectRetention.Unlock() - return mock.PutObjectRetentionFunc(contextMoqParam, putObjectRetentionInput) + return mock.PutObjectRetentionFunc(contextMoqParam, bucket, object, versionId, retention) } // PutObjectRetentionCalls gets all the calls that were made to PutObjectRetention. @@ -2239,12 +2274,18 @@ func (mock *BackendMock) PutObjectRetention(contextMoqParam context.Context, put // // len(mockedBackend.PutObjectRetentionCalls()) func (mock *BackendMock) PutObjectRetentionCalls() []struct { - ContextMoqParam context.Context - PutObjectRetentionInput *s3.PutObjectRetentionInput + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + Retention []byte } { var calls []struct { - ContextMoqParam context.Context - PutObjectRetentionInput *s3.PutObjectRetentionInput + ContextMoqParam context.Context + Bucket string + Object string + VersionId string + Retention []byte } mock.lockPutObjectRetention.RLock() calls = mock.calls.PutObjectRetention diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index fa6d6328..97509d5a 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -151,7 +151,17 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { } data, err := c.be.GetObjectRetention(ctx.Context(), bucket, key, versionId) - return SendXMLResponse(ctx, data, err, + if err != nil { + return SendXMLResponse(ctx, data, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } + + retention, err := auth.ParseObjectLockRetentionOutput(data) + return SendXMLResponse(ctx, retention, err, &MetaOpts{ Logger: c.logger, Action: "GetObjectRetention", @@ -179,7 +189,7 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { } data, err := c.be.GetObjectLegalHold(ctx.Context(), bucket, key, versionId) - return SendXMLResponse(ctx, data, err, + return SendXMLResponse(ctx, auth.ParseObjectLegalHoldOutput(data), err, &MetaOpts{ Logger: c.logger, Action: "GetObjectLegalHold", @@ -941,16 +951,6 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("object-lock") { parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) - var input types.ObjectLockConfiguration - if err := xml.Unmarshal(ctx.Body(), &input); err != nil { - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), - &MetaOpts{ - Logger: c.logger, - Action: "PutObjectLockConfiguration", - BucketOwner: parsedAcl.Owner, - }) - } - if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Acl: parsedAcl, AclPermission: types.PermissionWrite, @@ -967,10 +967,17 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { }) } - err := c.be.PutObjectLockConfiguration(ctx.Context(), &s3.PutObjectLockConfigurationInput{ - Bucket: &bucket, - ObjectLockConfiguration: &input, - }) + config, err := auth.ParseBucketLockConfigurationInput(ctx.Body()) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutObjectLockConfiguration", + BucketOwner: parsedAcl.Owner, + }) + } + + err = c.be.PutObjectLockConfiguration(ctx.Context(), bucket, config) return SendResponse(ctx, err, &MetaOpts{ Logger: c.logger, @@ -1211,7 +1218,6 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") versionId := ctx.Query("versionId") - bypassGovernanceRetention := ctx.Get("X-Amz-Bypass-Governance-Retention") acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) @@ -1313,24 +1319,6 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { } if ctx.Request().URI().QueryArgs().Has("retention") { - var retention types.ObjectLockRetention - if err := xml.Unmarshal(ctx.Body(), &retention); err != nil { - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), &MetaOpts{ - Logger: c.logger, - Action: "PutObjectRetention", - BucketOwner: parsedAcl.Owner, - }) - } - - if retention.RetainUntilDate == nil || retention.RetainUntilDate.Before(time.Now()) { - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate), - &MetaOpts{ - Logger: c.logger, - Action: "PutObjectRetention", - BucketOwner: parsedAcl.Owner, - }) - } - if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Acl: parsedAcl, AclPermission: types.PermissionWrite, @@ -1348,15 +1336,16 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } - pass := bypassGovernanceRetention == "True" + retention, err := auth.ParseObjectLockRetentionInput(ctx.Body()) + if err != nil { + return SendResponse(ctx, err, &MetaOpts{ + Logger: c.logger, + Action: "PutObjectRetention", + BucketOwner: parsedAcl.Owner, + }) + } - err := c.be.PutObjectRetention(ctx.Context(), &s3.PutObjectRetentionInput{ - Bucket: &bucket, - Key: &keyStart, - VersionId: &versionId, - Retention: &retention, - BypassGovernanceRetention: &pass, - }) + err = c.be.PutObjectRetention(ctx.Context(), bucket, keyStart, versionId, retention) return SendResponse(ctx, err, &MetaOpts{ Logger: c.logger, Action: "PutObjectRetention", @@ -1391,12 +1380,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } - err := c.be.PutObjectLegalHold(ctx.Context(), &s3.PutObjectLegalHoldInput{ - Bucket: &bucket, - Key: &keyStart, - VersionId: &versionId, - LegalHold: &legalHold, - }) + err := c.be.PutObjectLegalHold(ctx.Context(), bucket, keyStart, versionId, legalHold.Status == types.ObjectLockLegalHoldStatusOn) return SendResponse(ctx, err, &MetaOpts{ Logger: c.logger, Action: "PutObjectLegalHold", diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index a9f09a39..b522e38a 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -205,11 +205,18 @@ func TestS3ApiController_GetActions(t *testing.T) { GetObjectTaggingFunc: func(_ context.Context, bucket, object string) (map[string]string, error) { return map[string]string{"hello": "world"}, nil }, - GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*types.ObjectLockRetention, error) { - return &types.ObjectLockRetention{}, nil + GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string) ([]byte, error) { + result, err := json.Marshal(types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + }) + if err != nil { + return nil, err + } + return result, nil }, - GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*types.ObjectLockLegalHold, error) { - return &types.ObjectLockLegalHold{}, nil + GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*bool, error) { + result := true + return &result, nil }, }, } @@ -657,7 +664,7 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { PutBucketPolicyFunc: func(contextMoqParam context.Context, bucket string, policy []byte) error { return nil }, - PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, putObjectLockConfigurationInput *s3.PutObjectLockConfigurationInput) error { + PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string, config []byte) error { return nil }, }, @@ -919,10 +926,10 @@ func TestS3ApiController_PutActions(t *testing.T) { UploadPartCopyFunc: func(context.Context, *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) { return s3response.CopyObjectResult{}, nil }, - PutObjectLegalHoldFunc: func(contextMoqParam context.Context, putObjectLegalHoldInput *s3.PutObjectLegalHoldInput) error { + PutObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string, status bool) error { return nil }, - PutObjectRetentionFunc: func(contextMoqParam context.Context, putObjectRetentionInput *s3.PutObjectRetentionInput) error { + PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string, retention []byte) error { return nil }, GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {