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.",