From b4cd35f60b7e14cdbda49855ed586d5423e18730 Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Thu, 2 May 2024 15:15:04 -0400 Subject: [PATCH] feat: error refactoring and enable object lock in backends Added support to enable object lock on bucket creation in posix and azure backends. Implemented the logic to add object legal hold and retention on object creation in azure and posix backends. Added the functionality for HeadObject to return object lock related headers. Added integration tests for these features. --- auth/acl.go | 9 +- auth/bucket_policy.go | 2 +- auth/object_lock.go | 11 +- backend/azure/azure.go | 41 +++++- backend/posix/posix.go | 110 +++++++++++++--- s3api/controllers/base.go | 94 ++++++++++++-- s3err/s3err.go | 18 +++ tests/integration/group-tests.go | 14 ++- tests/integration/tests.go | 209 ++++++++++++++++++++++++++++--- 9 files changed, 445 insertions(+), 63 deletions(-) diff --git a/auth/acl.go b/auth/acl.go index 1053fca1..4aeb878a 100644 --- a/auth/acl.go +++ b/auth/acl.go @@ -17,6 +17,7 @@ package auth import ( "context" "encoding/json" + "errors" "fmt" "strings" @@ -292,13 +293,13 @@ func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) e return nil } - policy, err := be.GetBucketPolicy(ctx, opts.Bucket) - if err != nil { - return err + policy, policyErr := be.GetBucketPolicy(ctx, opts.Bucket) + if policyErr != nil && !errors.Is(policyErr, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) { + return policyErr } // If bucket policy is not set and the ACL is default, only the owner has access - if len(policy) == 0 && opts.Acl.ACL == "" && len(opts.Acl.Grantees) == 0 { + if errors.Is(policyErr, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) && opts.Acl.ACL == "" && len(opts.Acl.Grantees) == 0 { return s3err.GetAPIError(s3err.ErrAccessDenied) } diff --git a/auth/bucket_policy.go b/auth/bucket_policy.go index e703853d..4602269c 100644 --- a/auth/bucket_policy.go +++ b/auth/bucket_policy.go @@ -117,7 +117,7 @@ func ValidatePolicyDocument(policyBin []byte, bucket string, iam IAMService) err func verifyBucketPolicy(policy []byte, access, bucket, object string, action Action) error { // If bucket policy is not set - if len(policy) == 0 { + if policy == nil { return nil } diff --git a/auth/object_lock.go b/auth/object_lock.go index 6a5fbcbe..8f65bf0b 100644 --- a/auth/object_lock.go +++ b/auth/object_lock.go @@ -33,11 +33,6 @@ type BucketLockConfig struct { CreatedAt *time.Time } -type ObjectLockConfig struct { - LegalHoldEnabled bool - Retention *types.ObjectLockRetention -} - func ParseBucketLockConfigurationInput(input []byte) ([]byte, error) { var lockConfig types.ObjectLockConfiguration if err := xml.Unmarshal(input, &lockConfig); err != nil { @@ -172,12 +167,12 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ case types.ObjectLockRetentionModeGovernance: if !isAdminOrRoot { policy, err := be.GetBucketPolicy(ctx, bucket) + if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) { + return s3err.GetAPIError(s3err.ErrObjectLocked) + } 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) diff --git a/backend/azure/azure.go b/backend/azure/azure.go index c05fae4f..aa77bfd5 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -126,6 +126,21 @@ func (az *Azure) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, meta := map[string]*string{ string(keyAclCapital): backend.GetStringPtr(string(acl)), } + + if input.ObjectLockEnabledForBucket != nil && *input.ObjectLockEnabledForBucket { + now := time.Now() + defaultLock := auth.BucketLockConfig{ + Enabled: true, + CreatedAt: &now, + } + + defaultLockParsed, err := json.Marshal(defaultLock) + if err != nil { + return fmt.Errorf("parse default bucket lock state: %w", err) + } + + meta[string(keyBucketLock)] = backend.GetStringPtr(string(defaultLockParsed)) + } _, err := az.client.CreateContainer(ctx, *input.Bucket, &container.CreateOptions{Metadata: meta}) return azureErrToS3Err(err) } @@ -189,6 +204,28 @@ func (az *Azure) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, return "", azureErrToS3Err(err) } + // Set object legal hold + if po.ObjectLockLegalHoldStatus == types.ObjectLockLegalHoldStatusOn { + if err := az.PutObjectLegalHold(ctx, *po.Bucket, *po.Key, "", true); err != nil { + return "", err + } + } + + // Set object retention + if po.ObjectLockMode != "" { + retention := types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionMode(po.ObjectLockMode), + RetainUntilDate: po.ObjectLockRetainUntilDate, + } + retParsed, err := json.Marshal(retention) + if err != nil { + return "", fmt.Errorf("parse object lock retention: %w", err) + } + if err := az.PutObjectRetention(ctx, *po.Bucket, *po.Key, "", retParsed); err != nil { + return "", err + } + } + return string(*uploadResp.ETag), nil } @@ -235,7 +272,7 @@ func (az *Azure) GetBucketTagging(ctx context.Context, bucket string) (map[strin tagsJson, ok := resp.Metadata[string(keyTags)] if !ok { - return map[string]string{}, nil + return nil, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound) } var tags map[string]string @@ -888,7 +925,7 @@ func (az *Azure) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, er policyPtr, ok := props.Metadata[string(keyPolicy)] if !ok { - return []byte{}, nil + return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) } policy, err := base64.StdEncoding.DecodeString(*policyPtr) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index e4192b45..f38a4134 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -30,6 +30,7 @@ import ( "strconv" "strings" "syscall" + "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" @@ -229,6 +230,23 @@ func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, a return fmt.Errorf("set acl: %w", err) } + if input.ObjectLockEnabledForBucket != nil && *input.ObjectLockEnabledForBucket { + now := time.Now() + defaultLock := auth.BucketLockConfig{ + Enabled: true, + CreatedAt: &now, + } + + defaultLockParsed, err := json.Marshal(defaultLock) + if err != nil { + return fmt.Errorf("parse default bucket lock state: %w", err) + } + + if err := p.meta.StoreAttribute(bucket, "", bucketLockKey, defaultLockParsed); err != nil { + return fmt.Errorf("set default bucket lock: %w", err) + } + } + return nil } @@ -1226,6 +1244,7 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e } } + // Set object tagging if tagsStr != "" { err := p.PutObjectTagging(ctx, *po.Bucket, *po.Key, tags) if err != nil { @@ -1233,6 +1252,28 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e } } + // Set object legal hold + if po.ObjectLockLegalHoldStatus == types.ObjectLockLegalHoldStatusOn { + if err := p.PutObjectLegalHold(ctx, *po.Bucket, *po.Key, "", true); err != nil { + return "", err + } + } + + // Set object retention + if po.ObjectLockMode != "" { + retention := types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionMode(po.ObjectLockMode), + RetainUntilDate: po.ObjectLockRetainUntilDate, + } + retParsed, err := json.Marshal(retention) + if err != nil { + return "", fmt.Errorf("parse object lock retention: %w", err) + } + if err := p.PutObjectRetention(ctx, *po.Bucket, *po.Key, "", retParsed); err != nil { + return "", err + } + } + dataSum := hash.Sum(nil) etag := hex.EncodeToString(dataSum[:]) err = p.meta.StoreAttribute(*po.Bucket, *po.Key, etagkey, []byte(etag)) @@ -1414,12 +1455,15 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io etag = "" } + var tagCount *int32 tags, err := p.getAttrTags(bucket, object) - if err != nil { - return nil, fmt.Errorf("get object tags: %w", err) + if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) { + return nil, err + } + if tags != nil { + tgCount := int32(len(tags)) + tagCount = &tgCount } - - tagCount := int32(len(tags)) return &s3.GetObjectOutput{ AcceptRanges: &acceptRange, @@ -1429,7 +1473,7 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io ETag: &etag, LastModified: backend.GetTimePtr(fi.ModTime()), Metadata: userMetaData, - TagCount: &tagCount, + TagCount: tagCount, ContentRange: &contentRange, }, nil } @@ -1459,12 +1503,15 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io etag = "" } + var tagCount *int32 tags, err := p.getAttrTags(bucket, object) - if err != nil { - return nil, fmt.Errorf("get object tags: %w", err) + if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) { + return nil, err + } + if tags != nil { + tgCount := int32(len(tags)) + tagCount = &tgCount } - - tagCount := int32(len(tags)) return &s3.GetObjectOutput{ AcceptRanges: &acceptRange, @@ -1474,12 +1521,12 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io ETag: &etag, LastModified: backend.GetTimePtr(fi.ModTime()), Metadata: userMetaData, - TagCount: &tagCount, + TagCount: tagCount, ContentRange: &contentRange, }, nil } -func (p *Posix) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) { +func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) { if input.Bucket == nil { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -1517,16 +1564,39 @@ func (p *Posix) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.He size := fi.Size() - //TODO: Add object lock status properties + var objectLockLegalHoldStatus types.ObjectLockLegalHoldStatus + status, err := p.GetObjectLegalHold(ctx, bucket, object, "") + if err == nil { + if *status { + objectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOn + } else { + objectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOff + } + } + + var objectLockMode types.ObjectLockMode + var objectLockRetainUntilDate *time.Time + retention, err := p.GetObjectRetention(ctx, bucket, object, "") + if err == nil { + var config types.ObjectLockRetention + if err := json.Unmarshal(retention, &config); err == nil { + objectLockMode = types.ObjectLockMode(config.Mode) + objectLockRetainUntilDate = config.RetainUntilDate + } + } + //TODO: the method must handle multipart upload case return &s3.HeadObjectOutput{ - ContentLength: &size, - ContentType: &contentType, - ContentEncoding: &contentEncoding, - ETag: &etag, - LastModified: backend.GetTimePtr(fi.ModTime()), - Metadata: userMetaData, + ContentLength: &size, + ContentType: &contentType, + ContentEncoding: &contentEncoding, + ETag: &etag, + LastModified: backend.GetTimePtr(fi.ModTime()), + Metadata: userMetaData, + ObjectLockLegalHoldStatus: objectLockLegalHoldStatus, + ObjectLockMode: objectLockMode, + ObjectLockRetainUntilDate: objectLockRetainUntilDate, }, nil } @@ -1974,7 +2044,7 @@ func (p *Posix) getAttrTags(bucket, object string) (map[string]string, error) { return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, meta.ErrNoSuchKey) { - return tags, nil + return nil, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound) } if err != nil { return nil, fmt.Errorf("get tags: %w", err) @@ -2072,7 +2142,7 @@ func (p *Posix) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, err policy, err := p.meta.RetrieveAttribute(bucket, "", policykey) if errors.Is(err, meta.ErrNoSuchKey) { - return []byte{}, nil + return nil, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy) } if errors.Is(err, fs.ErrNotExist) { return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 851a8903..6d7890ab 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -1219,9 +1219,14 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { }) } + lockHeader := ctx.Get("X-Amz-Bucket-Object-Lock-Enabled") + // CLI provides "True", SDK - "true" + lockEnabled := lockHeader == "True" || lockHeader == "true" + err = c.be.CreateBucket(ctx.Context(), &s3.CreateBucketInput{ - Bucket: &bucket, - ObjectOwnership: types.ObjectOwnership(acct.Access), + Bucket: &bucket, + ObjectOwnership: types.ObjectOwnership(acct.Access), + ObjectLockEnabledForBucket: &lockEnabled, }, updAcl) return SendResponse(ctx, err, &MetaOpts{ @@ -1780,6 +1785,52 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } + legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold") + objLockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode") + objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date") + + if (objLockDate != "" && objLockModeHdr == "") || (objLockDate == "" && objLockModeHdr != "") { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders), + &MetaOpts{ + Logger: c.logger, + Action: "PutObject", + BucketOwner: parsedAcl.Owner, + }) + } + + var retainUntilDate *time.Time + if objLockDate != "" { + rDate, err := time.Parse(time.RFC3339, objLockDate) + if err != nil { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), + &MetaOpts{ + Logger: c.logger, + Action: "PutObject", + BucketOwner: parsedAcl.Owner, + }) + } + if rDate.Before(time.Now()) { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate), + &MetaOpts{ + Logger: c.logger, + Action: "PutObject", + BucketOwner: parsedAcl.Owner, + }) + } + retainUntilDate = &rDate + } + + if objLockModeHdr != "" && + objLockModeHdr != string(types.ObjectLockModeCompliance) && + objLockModeHdr != string(types.ObjectLockModeGovernance) { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), + &MetaOpts{ + Logger: c.logger, + Action: "PutObject", + BucketOwner: parsedAcl.Owner, + }) + } + var body io.Reader bodyi := ctx.Locals("body-reader") if bodyi != nil { @@ -1791,12 +1842,15 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { ctx.Locals("logReqBody", false) etag, err := c.be.PutObject(ctx.Context(), &s3.PutObjectInput{ - Bucket: &bucket, - Key: &keyStart, - ContentLength: &contentLength, - Metadata: metadata, - Body: body, - Tagging: &tagging, + Bucket: &bucket, + Key: &keyStart, + ContentLength: &contentLength, + Metadata: metadata, + Body: body, + Tagging: &tagging, + ObjectLockRetainUntilDate: retainUntilDate, + ObjectLockMode: types.ObjectLockMode(objLockModeHdr), + ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatus(legalHoldHdr), }) ctx.Response().Header.Set("ETag", etag) return SendResponse(ctx, err, @@ -2212,7 +2266,7 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error { if res.LastModified != nil { lastmod = res.LastModified.Format(timefmt) } - utils.SetResponseHeaders(ctx, []utils.CustomHeader{ + headers := []utils.CustomHeader{ { Key: "Content-Length", Value: fmt.Sprint(getint64(res.ContentLength)), @@ -2241,7 +2295,27 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error { Key: "x-amz-restore", Value: getstring(res.Restore), }, - }) + } + if res.ObjectLockMode != "" { + headers = append(headers, utils.CustomHeader{ + Key: "x-amz-object-lock-mode", + Value: string(res.ObjectLockMode), + }) + } + if res.ObjectLockLegalHoldStatus != "" { + headers = append(headers, utils.CustomHeader{ + Key: "x-amz-object-lock-legal-hold", + Value: string(res.ObjectLockLegalHoldStatus), + }) + } + if res.ObjectLockRetainUntilDate != nil { + retainUntilDate := res.ObjectLockRetainUntilDate.Format(time.RFC3339) + headers = append(headers, utils.CustomHeader{ + Key: "x-amz-object-lock-retain-until-date", + Value: retainUntilDate, + }) + } + utils.SetResponseHeaders(ctx, headers) return SendResponse(ctx, nil, &MetaOpts{ diff --git a/s3err/s3err.go b/s3err/s3err.go index 954904e4..f579b7bc 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -116,6 +116,9 @@ const ( ErrInvalidBucketObjectLockConfiguration ErrObjectLocked ErrPastObjectLockRetainDate + ErrNoSuchBucketPolicy + ErrBucketTaggingNotFound + ErrObjectLockInvalidHeaders ErrRequestTimeTooSkewed // Non-AWS errors @@ -431,6 +434,21 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "the retain until date must be in the future", HTTPStatusCode: http.StatusBadRequest, }, + ErrNoSuchBucketPolicy: { + Code: "NoSuchBucketPolicy", + Description: "The bucket policy does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrBucketTaggingNotFound: { + Code: "NoSuchTagSet", + Description: "The TagSet does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrObjectLockInvalidHeaders: { + Code: "InvalidRequest", + Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied", + HTTPStatusCode: http.StatusBadRequest, + }, ErrRequestTimeTooSkewed: { Code: "RequestTimeTooSkewed", Description: "The difference between the request time and the server's time is too large.", diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index d5689785..a64973ce 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -54,6 +54,7 @@ func TestCreateBucket(s *S3Conf) { CreateBucket_default_acl(s) CreateBucket_non_default_acl(s) CreateDeleteBucket_success(s) + CreateBucket_default_object_lock(s) } func TestHeadBucket(s *S3Conf) { @@ -81,6 +82,7 @@ func TestPutBucketTagging(s *S3Conf) { func TestGetBucketTagging(s *S3Conf) { GetBucketTagging_non_existing_bucket(s) + GetBucketTagging_unset_tags(s) GetBucketTagging_success(s) } @@ -94,6 +96,8 @@ func TestPutObject(s *S3Conf) { PutObject_non_existing_bucket(s) PutObject_special_chars(s) PutObject_invalid_long_tags(s) + PutObject_missing_object_lock_retention_config(s) + PutObject_with_object_lock(s) PutObject_success(s) PutObject_invalid_credentials(s) } @@ -165,6 +169,7 @@ func TestPutObjectTagging(s *S3Conf) { func TestGetObjectTagging(s *S3Conf) { GetObjectTagging_non_existing_object(s) + GetObjectTagging_unset_tags(s) GetObjectTagging_success(s) } @@ -275,7 +280,7 @@ func TestPutBucketPolicy(s *S3Conf) { func TestGetBucketPolicy(s *S3Conf) { GetBucketPolicy_non_existing_bucket(s) - GetBucketPolicy_default_empty_policy(s) + GetBucketPolicy_not_set(s) GetBucketPolicy_success(s) } @@ -449,6 +454,8 @@ func GetIntTests() IntTests { "PresignedAuth_expired_request": PresignedAuth_expired_request, "PresignedAuth_incorrect_secret_key": PresignedAuth_incorrect_secret_key, "PresignedAuth_PutObject_success": PresignedAuth_PutObject_success, + "PutObject_missing_object_lock_retention_config": PutObject_missing_object_lock_retention_config, + "PutObject_with_object_lock": PutObject_with_object_lock, "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, @@ -458,6 +465,7 @@ func GetIntTests() IntTests { "CreateDeleteBucket_success": CreateDeleteBucket_success, "CreateBucket_default_acl": CreateBucket_default_acl, "CreateBucket_non_default_acl": CreateBucket_non_default_acl, + "CreateBucket_default_object_lock": CreateBucket_default_object_lock, "HeadBucket_non_existing_bucket": HeadBucket_non_existing_bucket, "HeadBucket_success": HeadBucket_success, "ListBuckets_as_user": ListBuckets_as_user, @@ -470,6 +478,7 @@ func GetIntTests() IntTests { "PutBucketTagging_long_tags": PutBucketTagging_long_tags, "PutBucketTagging_success": PutBucketTagging_success, "GetBucketTagging_non_existing_bucket": GetBucketTagging_non_existing_bucket, + "GetBucketTagging_unset_tags": GetBucketTagging_unset_tags, "GetBucketTagging_success": GetBucketTagging_success, "DeleteBucketTagging_non_existing_object": DeleteBucketTagging_non_existing_object, "DeleteBucketTagging_success_status": DeleteBucketTagging_success_status, @@ -517,6 +526,7 @@ func GetIntTests() IntTests { "PutObjectTagging_long_tags": PutObjectTagging_long_tags, "PutObjectTagging_success": PutObjectTagging_success, "GetObjectTagging_non_existing_object": GetObjectTagging_non_existing_object, + "GetObjectTagging_unset_tags": GetObjectTagging_unset_tags, "GetObjectTagging_success": GetObjectTagging_success, "DeleteObjectTagging_non_existing_object": DeleteObjectTagging_non_existing_object, "DeleteObjectTagging_success_status": DeleteObjectTagging_success_status, @@ -591,7 +601,7 @@ func GetIntTests() IntTests { "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_not_set": GetBucketPolicy_not_set, "GetBucketPolicy_success": GetBucketPolicy_success, "DeleteBucketPolicy_non_existing_bucket": DeleteBucketPolicy_non_existing_bucket, "DeleteBucketPolicy_remove_before_setting": DeleteBucketPolicy_remove_before_setting, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 5d4111ba..9be7f882 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -1784,6 +1784,51 @@ func CreateBucket_non_default_acl(s *S3Conf) error { return nil } +func CreateBucket_default_object_lock(s *S3Conf) error { + testName := "CreateBucket_default_object_lock" + runF(testName) + + bucket := getBucketName() + lockEnabled := true + + client := s3.NewFromConfig(s.Config()) + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := client.CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: &bucket, + ObjectLockEnabledForBucket: &lockEnabled, + }) + cancel() + if err != nil { + failF("%v: %v", err) + return fmt.Errorf("%v: %w", testName, err) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: &bucket, + }) + cancel() + if err != nil { + failF("%v: %v", err) + return fmt.Errorf("%v: %w", testName, err) + } + + if resp.ObjectLockConfiguration.ObjectLockEnabled != types.ObjectLockEnabledEnabled { + failF("%v: expected object lock to be enabled", testName) + return fmt.Errorf("%v: expected object lock to be enabled", testName) + } + + err = teardown(s, bucket) + if err != nil { + failF("%v: %v", err) + return fmt.Errorf("%v: %w", testName, err) + } + + passF(testName) + return nil +} + func HeadBucket_non_existing_bucket(s *S3Conf) error { testName := "HeadBucket_non_existing_bucket" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -2183,6 +2228,21 @@ func GetBucketTagging_non_existing_bucket(s *S3Conf) error { }) } +func GetBucketTagging_unset_tags(s *S3Conf) error { + testName := "GetBucketTagging_unset_tags" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{ + Bucket: &bucket, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)); err != nil { + return err + } + return nil + }) +} + func GetBucketTagging_success(s *S3Conf) error { testName := "GetBucketTagging_success" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -2371,6 +2431,107 @@ func PutObject_invalid_long_tags(s *S3Conf) error { }) } +func PutObject_missing_object_lock_retention_config(s *S3Conf) error { + testName := "PutObject_missing_object_lock_retention_config" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + key := "my-obj" + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &key, + ObjectLockMode: types.ObjectLockModeCompliance, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + return err + } + + retainDate := time.Now().Add(time.Hour * 48) + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &key, + ObjectLockRetainUntilDate: &retainDate, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + return err + } + + return nil + }) +} + +func PutObject_with_object_lock(s *S3Conf) error { + testName := "PutObject_with_object_lock" + runF(testName) + bucket, obj, lockStatus := getBucketName(), "my-obj", true + + client := s3.NewFromConfig(s.Config()) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := client.CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: &bucket, + ObjectLockEnabledForBucket: &lockStatus, + }) + cancel() + if err != nil { + failF("%v: %v", testName, err) + return fmt.Errorf("%v: %w", testName, err) + } + + retainDate := time.Now().Add(time.Hour * 48) + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn, + ObjectLockMode: types.ObjectLockModeCompliance, + ObjectLockRetainUntilDate: &retainDate, + }) + cancel() + if err != nil { + failF("%v: %v", testName, err) + return fmt.Errorf("%v: %w", testName, err) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + out, err := client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + failF("%v: %v", testName, err) + return fmt.Errorf("%v: %w", testName, err) + } + + if out.ObjectLockMode != types.ObjectLockModeCompliance { + failF("%v: expected object lock mode to be %v, instead got %v", testName, types.ObjectLockModeCompliance, out.ObjectLockMode) + return fmt.Errorf("%v: expected object lock mode to be %v, instead got %v", testName, types.ObjectLockModeCompliance, out.ObjectLockMode) + } + if out.ObjectLockLegalHoldStatus != types.ObjectLockLegalHoldStatusOn { + failF("%v: expected object lock mode to be %v, instead got %v", testName, types.ObjectLockLegalHoldStatusOn, out.ObjectLockLegalHoldStatus) + return fmt.Errorf("%v: expected object lock mode to be %v, instead got %v", testName, types.ObjectLockLegalHoldStatusOn, out.ObjectLockLegalHoldStatus) + } + + if err := changeBucketObjectLockStatus(client, bucket, false); err != nil { + failF("%v: %v", err) + return fmt.Errorf("%v: %w", testName, err) + } + + err = teardown(s, bucket) + if err != nil { + failF("%v: %v", err) + return fmt.Errorf("%v: %w", testName, err) + } + + passF(testName) + return nil +} + func PutObject_success(s *S3Conf) error { testName := "PutObject_success" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -2418,7 +2579,11 @@ func HeadObject_success(s *S3Conf) error { "key2": "val2", } - _, _, err := putObjectWithData(dataLen, &s3.PutObjectInput{Bucket: &bucket, Key: &obj, Metadata: meta}, s3client) + _, _, err := putObjectWithData(dataLen, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + Metadata: meta, + }, s3client) if err != nil { return err } @@ -3757,6 +3922,26 @@ func GetObjectTagging_non_existing_object(s *S3Conf) error { }) } +func GetObjectTagging_unset_tags(s *S3Conf) error { + testName := "GetObjectTagging_unset_tags" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + if err := putObjects(s3client, []string{obj}, bucket); err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)); err != nil { + return err + } + return nil + }) +} + func GetObjectTagging_success(s *S3Conf) error { testName := "PutObjectTagging_success" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -5919,22 +6104,18 @@ func GetBucketPolicy_non_existing_bucket(s *S3Conf) error { }) } -func GetBucketPolicy_default_empty_policy(s *S3Conf) error { - testName := "GetBucketPolicy_default_empty_policy" +func GetBucketPolicy_not_set(s *S3Conf) error { + testName := "GetBucketPolicy_not_set" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) - out, err := s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{ + _, err := s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{ Bucket: &bucket, }) cancel() - if err != nil { + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)); err != nil { return err } - if out.Policy != nil { - return fmt.Errorf("expected policy to be nil, instead got %s", *out.Policy) - } - return nil }) } @@ -6026,19 +6207,15 @@ func DeleteBucketPolicy_success(s *S3Conf) error { } ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) - out, err := s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{ + _, err = s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{ Bucket: &bucket, }) cancel() - if err != nil { + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)); err != nil { return err } - if out.Policy != nil { - return fmt.Errorf("expected policy to be nil, instead got %s", *out.Policy) - } - - return err + return nil }) }