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 }) }