From 96f81b17ac7faed55cb23f6efd82e29267e9465b Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 4 Sep 2026 12:54:59 -0400 Subject: [PATCH 01/15] fix: allow far-future If-Modified-Since --- s3api/controllers/object-get.go | 5 ++++ s3api/controllers/object-get_test.go | 1 + s3api/utils/precondition.go | 24 ++++----------- s3api/utils/precondition_test.go | 44 ++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 s3api/utils/precondition_test.go diff --git a/s3api/controllers/object-get.go b/s3api/controllers/object-get.go index 87b0e835c..dc35a21ab 100644 --- a/s3api/controllers/object-get.go +++ b/s3api/controllers/object-get.go @@ -502,6 +502,11 @@ func (c S3ApiController) GetObject(ctx fiber.Ctx) (*Response, error) { headers = map[string]*string{ "x-amz-delete-marker": utils.GetStringPtr("true"), "Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt), + // A 304 Not Modified must carry the object's ETag (RFC 7232 + // §4.1). Backends surface it by returning a non-nil result + // alongside the not-modified error; nil ETag values are + // dropped when the response is written. + "ETag": res.ETag, } } return &Response{ diff --git a/s3api/controllers/object-get_test.go b/s3api/controllers/object-get_test.go index add982800..8674bc8c4 100644 --- a/s3api/controllers/object-get_test.go +++ b/s3api/controllers/object-get_test.go @@ -757,6 +757,7 @@ func TestS3ApiController_GetObject(t *testing.T) { Headers: map[string]*string{ "x-amz-delete-marker": utils.GetStringPtr("true"), "Last-Modified": utils.GetStringPtr(tm.UTC().Format(timefmt)), + "ETag": nil, }, MetaOpts: &MetaOptions{ BucketOwner: "root", diff --git a/s3api/utils/precondition.go b/s3api/utils/precondition.go index 868b0a1d9..db82c2393 100644 --- a/s3api/utils/precondition.go +++ b/s3api/utils/precondition.go @@ -102,28 +102,16 @@ func ParsePreconditionDateHeader(date string) *time.Time { if date == "" { return nil } - // try to parse as RFC1123 - parsed, err := time.Parse(time.RFC1123, date) - if err == nil { - // ignore future dates - if parsed.After(time.Now()) { - return nil - } - + // A future date is not ignored: S3 evaluates If-Modified-Since / + // If-Unmodified-Since against the object's last-modified time regardless + // (a future If-Modified-Since on an unmodified object yields 304). The + // comparison is EvaluatePreconditions' job, not the parser's. + if parsed, err := time.Parse(time.RFC1123, date); err == nil { return &parsed } - - // try to parse as RFC3339 - parsed, err = time.Parse(time.RFC3339, date) - if err == nil { - // ignore future dates - if parsed.After(time.Now()) { - return nil - } - + if parsed, err := time.Parse(time.RFC3339, date); err == nil { return &parsed } - return nil } diff --git a/s3api/utils/precondition_test.go b/s3api/utils/precondition_test.go new file mode 100644 index 000000000..88e41406c --- /dev/null +++ b/s3api/utils/precondition_test.go @@ -0,0 +1,44 @@ +// 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 utils + +import "testing" + +func TestParsePreconditionDateHeader(t *testing.T) { + if got := ParsePreconditionDateHeader(""); got != nil { + t.Errorf("empty date = %v, want nil", got) + } + if got := ParsePreconditionDateHeader("not a date"); got != nil { + t.Errorf("unparseable date = %v, want nil", got) + } + if got := ParsePreconditionDateHeader("Mon, 02 Jan 2006 15:04:05 GMT"); got == nil { + t.Error("past RFC1123 date parsed to nil, want a time") + } + if got := ParsePreconditionDateHeader("2006-01-02T15:04:05Z"); got == nil { + t.Error("RFC3339 date parsed to nil, want a time") + } + + // A future date must be honored, not dropped: S3 evaluates + // If-Modified-Since against the object's last-modified time regardless of + // whether the date is in the future (a future If-Modified-Since on an + // unmodified object yields 304). See AWS S3 GetObject API reference. + future := ParsePreconditionDateHeader("Fri, 29 Oct 2100 19:43:31 GMT") + if future == nil { + t.Fatal("future RFC1123 date parsed to nil, want a time (future dates must not be ignored)") + } + if future.Year() != 2100 { + t.Errorf("future date year = %d, want 2100", future.Year()) + } +} From 8e238fb540e08f15f32e424f2f827bd6f5900a59 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 4 Sep 2026 14:17:44 -0400 Subject: [PATCH 02/15] fix: invalid control characters in URL --- .../bucket-object-name-validator.go | 12 +++++- s3api/utils/name_validate.go | 12 ++++++ s3api/utils/name_validate_test.go | 41 +++++++++++++++++++ s3err/s3err.go | 2 +- 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 s3api/utils/name_validate_test.go diff --git a/s3api/middlewares/bucket-object-name-validator.go b/s3api/middlewares/bucket-object-name-validator.go index 5603239ce..37073c7d5 100644 --- a/s3api/middlewares/bucket-object-name-validator.go +++ b/s3api/middlewares/bucket-object-name-validator.go @@ -33,8 +33,16 @@ func BucketObjectNameValidator() fiber.Handler { // check if the provided object name is valid // skip for empty objects: e.g bucket operations: HeadBucket... - if object != "" && !utils.IsObjectNameValid(object) { - return s3err.GetAPIError(s3err.ErrBadRequest) + if object != "" { + // A C1 control character in the key is unparseable to S3: reject + // pre-auth with InvalidURI, matching AWS. C0 controls (CR/LF/ESC) + // are legal and handled by the general validity check below. + if utils.ContainsC1ControlChar(object) { + return s3err.GetAPIError(s3err.ErrInvalidURI) + } + if !utils.IsObjectNameValid(object) { + return s3err.GetAPIError(s3err.ErrBadRequest) + } } return nil diff --git a/s3api/utils/name_validate.go b/s3api/utils/name_validate.go index 4f8250e13..5aabc874f 100644 --- a/s3api/utils/name_validate.go +++ b/s3api/utils/name_validate.go @@ -22,3 +22,15 @@ func IsObjectNameValid(name string) bool { return isObjectLocal(name) } + +// ContainsC1ControlChar reports whether s contains a C1 control character +// (U+0080–U+009F). S3 rejects object keys containing these with InvalidURI; C0 +// controls (CR, LF, ESC, ...) are accepted and round-trip. +func ContainsC1ControlChar(s string) bool { + for _, r := range s { + if r >= 0x80 && r <= 0x9f { + return true + } + } + return false +} diff --git a/s3api/utils/name_validate_test.go b/s3api/utils/name_validate_test.go new file mode 100644 index 000000000..4c58e1166 --- /dev/null +++ b/s3api/utils/name_validate_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 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 utils_test + +import ( + "testing" + + "github.com/fil-forge/versitygw/s3api/utils" +) + +func TestContainsC1ControlChar(t *testing.T) { + cases := []struct { + name string + key string + want bool + }{ + {"c1 control U+008A (object-crud-0037 key)", "®Š-", true}, + {"crlf is c0, allowed", "test\r\nCRLF.txt", false}, + {"escape is c0, allowed", "testescape.txt", false}, + {"registered sign is above the c1 range", "®-plain", false}, + {"plain ascii", "hello/world.txt", false}, + {"empty", "", false}, + } + for _, c := range cases { + if got := utils.ContainsC1ControlChar(c.key); got != c.want { + t.Errorf("%s: ContainsC1ControlChar(%q) = %v, want %v", c.name, c.key, got, c.want) + } + } +} diff --git a/s3err/s3err.go b/s3err/s3err.go index 1b1348ff7..2d209cf90 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -479,7 +479,7 @@ var errorCodeResponse = map[ErrorCode]APIError{ }, ErrInvalidURI: { Code: "InvalidURI", - Description: "The specified URI couldn't be parsed.", + Description: "Couldn't parse the specified URI.", HTTPStatusCode: http.StatusBadRequest, }, ErrObjectLockConfigurationNotFound: { From 3c7f12179024a3ab7258f0885facbc9d4fa88769 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 4 Sep 2026 14:30:36 -0400 Subject: [PATCH 03/15] fix: quiet mode for DeleteObjects --- s3api/controllers/bucket-post.go | 1 + s3response/deleteobjects_test.go | 44 ++++++++++++++++++++++++++++++++ s3response/s3response.go | 1 + 3 files changed, 46 insertions(+) create mode 100644 s3response/deleteobjects_test.go diff --git a/s3api/controllers/bucket-post.go b/s3api/controllers/bucket-post.go index 754a14d73..e4578bbfe 100644 --- a/s3api/controllers/bucket-post.go +++ b/s3api/controllers/bucket-post.go @@ -85,6 +85,7 @@ func (c S3ApiController) DeleteObjects(ctx fiber.Ctx) (*Response, error) { Bucket: &bucket, Delete: &types.Delete{ Objects: dObj.Objects, + Quiet: &dObj.Quiet, }, }) return &Response{ diff --git a/s3response/deleteobjects_test.go b/s3response/deleteobjects_test.go new file mode 100644 index 000000000..768a45ef7 --- /dev/null +++ b/s3response/deleteobjects_test.go @@ -0,0 +1,44 @@ +// 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 s3response + +import ( + "encoding/xml" + "testing" +) + +// TestDeleteObjectsUnmarshalQuiet locks parsing of the flag from a +// DeleteObjects request body. Without it the controller cannot honor quiet +// mode and always returns the Deleted list. +func TestDeleteObjectsUnmarshalQuiet(t *testing.T) { + cases := []struct { + name string + body string + want bool + }{ + {"quiet true", `ktrue`, true}, + {"quiet false", `kfalse`, false}, + {"quiet absent", `k`, false}, + } + for _, c := range cases { + var d DeleteObjects + if err := xml.Unmarshal([]byte(c.body), &d); err != nil { + t.Fatalf("%s: unmarshal: %v", c.name, err) + } + if d.Quiet != c.want { + t.Errorf("%s: Quiet = %v, want %v", c.name, d.Quiet, c.want) + } + } +} diff --git a/s3response/s3response.go b/s3response/s3response.go index 6b115f7ce..ca0f7f1e2 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -292,6 +292,7 @@ func (t *Tagging) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { type DeleteObjects struct { Objects []types.ObjectIdentifier `xml:"Object"` + Quiet bool `xml:"Quiet"` } type DeleteResult struct { From 11a6687b3df5d24a53b56d3604d0f75587db1908 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 4 Sep 2026 14:41:19 -0400 Subject: [PATCH 04/15] fix: empty x-amz-object-attributes error code --- s3err/s3err.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s3err/s3err.go b/s3err/s3err.go index 2d209cf90..0c12c3062 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -523,7 +523,7 @@ var errorCodeResponse = map[ErrorCode]APIError{ HTTPStatusCode: http.StatusNotFound, }, ErrObjectAttributesInvalidHeader: { - Code: "InvalidRequest", + Code: "InvalidArgument", Description: "The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty", HTTPStatusCode: http.StatusBadRequest, }, From 458572166cfa92e79e3085daf3d34deafa628afb Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 4 Sep 2026 14:58:14 -0400 Subject: [PATCH 05/15] feat: optionally allow path traversal in object keys --- .../bucket-object-name-validator.go | 10 +- s3api/router.go | 220 +++++++++--------- s3api/server.go | 8 + s3api/utils/name_validate.go | 13 ++ 4 files changed, 138 insertions(+), 113 deletions(-) diff --git a/s3api/middlewares/bucket-object-name-validator.go b/s3api/middlewares/bucket-object-name-validator.go index 37073c7d5..7e2dad46f 100644 --- a/s3api/middlewares/bucket-object-name-validator.go +++ b/s3api/middlewares/bucket-object-name-validator.go @@ -20,9 +20,11 @@ import ( "github.com/gofiber/fiber/v3" ) -// BucketObjectNameValidator extracts and validates -// the bucket and object names from the request URI. -func BucketObjectNameValidator() fiber.Handler { +// BucketObjectNameValidator extracts and validates the bucket and object names +// from the request URI. checkTraversal enables the path-traversal (non-local +// key) rejection, which suits filesystem-backed backends; backends that store +// keys opaquely pass false so keys like "../file.txt" are accepted literally. +func BucketObjectNameValidator(checkTraversal bool) fiber.Handler { return func(ctx fiber.Ctx) error { bucket, object := parsePath(ctx.Path()) @@ -40,7 +42,7 @@ func BucketObjectNameValidator() fiber.Handler { if utils.ContainsC1ControlChar(object) { return s3err.GetAPIError(s3err.ErrInvalidURI) } - if !utils.IsObjectNameValid(object) { + if !utils.IsObjectNameValidWithTraversal(object, checkTraversal) { return s3err.GetAPIError(s3err.ErrBadRequest) } } diff --git a/s3api/router.go b/s3api/router.go index d05a4ab91..aa266d803 100644 --- a/s3api/router.go +++ b/s3api/router.go @@ -29,22 +29,23 @@ import ( ) type S3ApiRouter struct { - WithAdmSrv bool - app *fiber.App - be backend.Backend - iam auth.IAMService - logger s3log.AuditLogger - aLogger s3log.AuditLogger - evs s3event.S3EventSender - mm metrics.Manager - root middlewares.RootUserConfig - Ctrl controllers.S3ApiController - readonly bool - disableACL bool - region string - virtualDomain string - corsAllowOrigin string - mpMaxParts int + WithAdmSrv bool + app *fiber.App + be backend.Backend + iam auth.IAMService + logger s3log.AuditLogger + aLogger s3log.AuditLogger + evs s3event.S3EventSender + mm metrics.Manager + root middlewares.RootUserConfig + Ctrl controllers.S3ApiController + readonly bool + disableACL bool + disableObjNameTraversal bool + region string + virtualDomain string + corsAllowOrigin string + mpMaxParts int } func (sa *S3ApiRouter) Init() { @@ -186,6 +187,7 @@ func (sa *S3ApiRouter) Init() { bucketRouter := sa.app.Group("/:bucket") objectRouter := sa.app.Group("/:bucket/*") applyBucketCORS := middlewares.ApplyBucketCORS(sa.be, middlewares.BucketFromPath, sa.corsAllowOrigin) + objNameValidator := middlewares.BucketObjectNameValidator(!sa.disableObjNameTraversal) // PUT bucket operations bucketRouter.Put("", @@ -194,7 +196,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutBucketTagging, metrics.ActionPutBucketTagging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, applyBucketCORS, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketTagging, auth.PutBucketTaggingAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), @@ -208,7 +210,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutBucketOwnershipControls, metrics.ActionPutBucketOwnershipControls, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketOwnershipControls, auth.PutBucketOwnershipControlsAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -222,7 +224,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutBucketVersioning, metrics.ActionPutBucketVersioning, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketVersioning, auth.PutBucketVersioningAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -236,7 +238,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutObjectLockConfiguration, metrics.ActionPutObjectLockConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutObjectLockConfiguration, auth.PutBucketObjectLockConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -250,7 +252,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutBucketCors, metrics.ActionPutBucketCors, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketCors, auth.PutBucketCorsAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -264,7 +266,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutBucketPolicy, metrics.ActionPutBucketPolicy, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketPolicy, auth.PutBucketPolicyAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -278,7 +280,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutBucketAcl, metrics.ActionPutBucketAcl, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketAcl, auth.PutBucketAclAction, auth.PermissionWriteAcp, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -292,7 +294,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketAnalyticsConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketAnalyticsConfiguration, auth.PutAnalyticsConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -305,7 +307,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketEncryption, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketEncryption, auth.PutEncryptionConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -318,7 +320,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketIntelligentTieringConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketIntelligentTieringConfiguration, auth.PutIntelligentTieringConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -331,7 +333,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketInventoryConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketInventoryConfiguration, auth.PutInventoryConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -344,7 +346,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketLifecycleConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketLifecycleConfiguration, auth.PutLifecycleConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -357,7 +359,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketLogging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketLogging, auth.PutBucketLoggingAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -370,7 +372,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketRequestPayment, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketRequestPayment, auth.PutBucketRequestPaymentAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -383,7 +385,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketMetricsConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketMetricsConfiguration, auth.PutMetricsConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -396,7 +398,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketReplication, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketReplication, auth.PutReplicationConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -409,7 +411,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutPublicAccessBlock, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutPublicAccessBlock, auth.PutBucketPublicAccessBlockAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -422,7 +424,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketNotificationConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketNotificationConfiguration, auth.PutBucketNotificationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -435,7 +437,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionPutBucketAccelerateConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketAccelerateConfiguration, auth.PutAccelerateConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -448,7 +450,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutBucketWebsite, metrics.ActionPutBucketWebsite, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutBucketWebsite, auth.PutBucketWebsiteAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -462,7 +464,7 @@ func (sa *S3ApiRouter) Init() { ctrl.CreateBucket, metrics.ActionCreateBucket, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionCreateBucket, auth.CreateBucketAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -488,7 +490,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HeadBucket, metrics.ActionHeadBucket, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionHeadBucket, auth.ListBucketAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, false, false), @@ -515,7 +517,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteBucketTagging, metrics.ActionDeleteBucketTagging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketTagging, auth.PutBucketTaggingAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -528,7 +530,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteBucketOwnershipControls, metrics.ActionDeleteBucketOwnershipControls, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketOwnershipControls, auth.PutBucketOwnershipControlsAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -541,7 +543,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteBucketPolicy, metrics.ActionDeleteBucketPolicy, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketPolicy, auth.PutBucketPolicyAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -554,7 +556,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteBucketCors, metrics.ActionDeleteBucketCors, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketCors, auth.PutBucketCorsAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -567,7 +569,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeleteBucketAnalyticsConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketAnalyticsConfiguration, auth.PutAnalyticsConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -580,7 +582,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeleteBucketEncryption, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketEncryption, auth.PutEncryptionConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -593,7 +595,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeleteBucketIntelligentTieringConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketIntelligentTieringConfiguration, auth.PutIntelligentTieringConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -606,7 +608,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeleteBucketInventoryConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketInventoryConfiguration, auth.PutInventoryConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -619,7 +621,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeleteBucketLifecycle, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketLifecycle, auth.PutLifecycleConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -632,7 +634,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeleteBucketMetricsConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketMetricsConfiguration, auth.PutMetricsConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -645,7 +647,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeleteBucketReplication, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketReplication, auth.PutReplicationConfigurationAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -658,7 +660,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionDeletePublicAccessBlock, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeletePublicAccessBlock, auth.PutBucketPublicAccessBlockAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -671,7 +673,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteBucketWebsite, metrics.ActionDeleteBucketWebsite, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucketWebsite, auth.DeleteBucketWebsiteAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -684,7 +686,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteBucket, metrics.ActionDeleteBucket, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteBucket, auth.DeleteBucketAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -711,7 +713,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketLocation, metrics.ActionGetBucketLocation, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketLocation, auth.GetBucketLocationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -725,7 +727,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketTagging, metrics.ActionGetBucketTagging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketTagging, auth.GetBucketTaggingAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -738,7 +740,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketOwnershipControls, metrics.ActionGetBucketOwnershipControls, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketOwnershipControls, auth.GetBucketOwnershipControlsAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -751,7 +753,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketVersioning, metrics.ActionGetBucketVersioning, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketVersioning, auth.GetBucketVersioningAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -764,7 +766,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketPolicy, metrics.ActionGetBucketPolicy, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketPolicy, auth.GetBucketPolicyAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -777,7 +779,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketCors, metrics.ActionGetBucketCors, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketCors, auth.GetBucketCorsAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -790,7 +792,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetObjectLockConfiguration, metrics.ActionGetObjectLockConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObjectLockConfiguration, auth.GetBucketObjectLockConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -803,7 +805,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketAcl, metrics.ActionGetBucketAcl, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketAcl, auth.GetBucketAclAction, auth.PermissionReadAcp, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, false, false), @@ -816,7 +818,7 @@ func (sa *S3ApiRouter) Init() { ctrl.ListMultipartUploads, metrics.ActionListMultipartUploads, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListMultipartUploads, auth.ListBucketMultipartUploadsAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -829,7 +831,7 @@ func (sa *S3ApiRouter) Init() { ctrl.ListObjectVersions, metrics.ActionListObjectVersions, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListObjectVersions, auth.ListBucketVersionsAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -842,7 +844,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketPolicyStatus, metrics.ActionGetBucketPolicyStatus, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketPolicyStatus, auth.GetBucketPolicyStatusAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -855,7 +857,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketAnalyticsConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketAnalyticsConfiguration, auth.GetAnalyticsConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -868,7 +870,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionListBucketAnalyticsConfigurations, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListBucketAnalyticsConfigurations, auth.GetAnalyticsConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -881,7 +883,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketEncryption, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketEncryption, auth.GetEncryptionConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -894,7 +896,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketIntelligentTieringConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketIntelligentTieringConfiguration, auth.GetIntelligentTieringConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -907,7 +909,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionListBucketIntelligentTieringConfigurations, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListBucketIntelligentTieringConfigurations, auth.GetIntelligentTieringConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -920,7 +922,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketInventoryConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketInventoryConfiguration, auth.GetInventoryConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -933,7 +935,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionListBucketInventoryConfigurations, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListBucketInventoryConfigurations, auth.GetInventoryConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -946,7 +948,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketLifecycleConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketLifecycleConfiguration, auth.GetLifecycleConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -959,7 +961,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketLogging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketLogging, auth.GetBucketLoggingAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -972,7 +974,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketRequestPayment, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketRequestPayment, auth.GetBucketRequestPaymentAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -985,7 +987,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketMetricsConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketMetricsConfiguration, auth.GetMetricsConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -998,7 +1000,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionListBucketMetricsConfigurations, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListBucketMetricsConfigurations, auth.GetMetricsConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1011,7 +1013,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketReplication, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketReplication, auth.GetReplicationConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1024,7 +1026,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetPublicAccessBlock, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetPublicAccessBlock, auth.GetBucketPublicAccessBlockAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1037,7 +1039,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketNotificationConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketNotificationConfiguration, auth.GetBucketNotificationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1050,7 +1052,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)), metrics.ActionGetBucketAccelerateConfiguration, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketAccelerateConfiguration, auth.GetAccelerateConfigurationAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1063,7 +1065,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetBucketWebsite, metrics.ActionGetBucketWebsite, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetBucketWebsite, auth.GetBucketWebsiteAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1077,7 +1079,7 @@ func (sa *S3ApiRouter) Init() { ctrl.ListObjectsV2, metrics.ActionListObjectsV2, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListObjectsV2, auth.ListBucketAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1089,7 +1091,7 @@ func (sa *S3ApiRouter) Init() { ctrl.ListObjects, metrics.ActionListObjects, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListObjects, auth.ListBucketAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1117,7 +1119,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteObjects, metrics.ActionDeleteObjects, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteObjects, auth.DeleteObjectAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1131,7 +1133,7 @@ func (sa *S3ApiRouter) Init() { ctrl.POSTObject, metrics.ActionPostObject, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePostObject(sa.root, sa.iam, sa.region), middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPostObject, auth.PutObjectAction, auth.PermissionWrite, sa.region, false), applyBucketCORS, @@ -1156,7 +1158,7 @@ func (sa *S3ApiRouter) Init() { ctrl.HeadObject, metrics.ActionHeadObject, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionHeadObject, auth.GetObjectAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, false, false), @@ -1196,7 +1198,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetObjectTagging, metrics.ActionGetObjectTagging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObjectTagging, auth.GetObjectTaggingAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1209,7 +1211,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetObjectRetention, metrics.ActionGetObjectRetention, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObjectRetention, auth.GetObjectRetentionAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1222,7 +1224,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetObjectLegalHold, metrics.ActionGetObjectLegalHold, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObjectLegalHold, auth.GetObjectLegalHoldAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1235,7 +1237,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetObjectAcl, metrics.ActionGetObjectAcl, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObjectAcl, auth.GetObjectAclAction, auth.PermissionReadAcp, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1248,7 +1250,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetObjectAttributes, metrics.ActionGetObjectAttributes, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObjectAttributes, auth.GetObjectAttributesAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1261,7 +1263,7 @@ func (sa *S3ApiRouter) Init() { ctrl.ListParts, metrics.ActionListParts, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionListParts, auth.ListMultipartUploadPartsAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1273,7 +1275,7 @@ func (sa *S3ApiRouter) Init() { ctrl.GetObject, metrics.ActionGetObject, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObject, auth.GetObjectAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1301,7 +1303,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteObjectTagging, metrics.ActionDeleteObjectTagging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteObjectTagging, auth.DeleteObjectTaggingAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1314,7 +1316,7 @@ func (sa *S3ApiRouter) Init() { ctrl.AbortMultipartUpload, metrics.ActionAbortMultipartUpload, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionAbortMultipartUpload, auth.AbortMultipartUploadAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1326,7 +1328,7 @@ func (sa *S3ApiRouter) Init() { ctrl.DeleteObject, metrics.ActionDeleteObject, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionDeleteObject, auth.DeleteObjectAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1355,7 +1357,7 @@ func (sa *S3ApiRouter) Init() { ctrl.RestoreObject, metrics.ActionRestoreObject, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionRestoreObject, auth.RestoreObjectAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1370,7 +1372,7 @@ func (sa *S3ApiRouter) Init() { ctrl.SelectObjectContent, metrics.ActionSelectObjectContent, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionSelectObjectContent, auth.GetObjectAction, auth.PermissionRead, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1384,7 +1386,7 @@ func (sa *S3ApiRouter) Init() { ctrl.CompleteMultipartUpload, metrics.ActionCompleteMultipartUpload, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionCompleteMultipartUpload, auth.PutObjectAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1397,7 +1399,7 @@ func (sa *S3ApiRouter) Init() { ctrl.CreateMultipartUpload, metrics.ActionCreateMultipartUpload, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionCreateMultipartUpload, auth.PutObjectAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1412,7 +1414,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutObjectTagging, metrics.ActionPutObjectTagging, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, applyBucketCORS, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutObjectTagging, auth.PutObjectTaggingAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), @@ -1426,7 +1428,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutObjectRetention, metrics.ActionPutObjectRetention, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutObjectRetention, auth.PutObjectRetentionAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1440,7 +1442,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutObjectLegalHold, metrics.ActionPutObjectLegalHold, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutObjectLegalHold, auth.PutObjectLegalHoldAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1454,7 +1456,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutObjectAcl, metrics.ActionPutObjectAcl, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutObjectAcl, auth.PutObjectAclAction, auth.PermissionWriteAcp, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1469,7 +1471,7 @@ func (sa *S3ApiRouter) Init() { ctrl.UploadPartCopy, metrics.ActionUploadPartCopy, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionUploadPartCopy, auth.PutObjectAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false), @@ -1482,7 +1484,7 @@ func (sa *S3ApiRouter) Init() { ctrl.UploadPart, metrics.ActionUploadPart, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionUploadPart, auth.PutObjectAction, auth.PermissionWrite, sa.region, true), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, true), middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, true, true, false), @@ -1523,7 +1525,7 @@ func (sa *S3ApiRouter) Init() { ctrl.CopyObject, metrics.ActionCopyObject, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, applyBucketCORS, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionCopyObject, auth.PutObjectAction, auth.PermissionWrite, sa.region, false), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false), @@ -1535,7 +1537,7 @@ func (sa *S3ApiRouter) Init() { ctrl.PutObject, metrics.ActionPutObject, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, applyBucketCORS, middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionPutObject, auth.PutObjectAction, auth.PermissionWrite, sa.region, true), middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, true), @@ -1547,7 +1549,7 @@ func (sa *S3ApiRouter) Init() { sa.app.Options("/:bucket", middlewares.ApplyBucketCORSPreflightFallback(sa.be, sa.corsAllowOrigin), controllers.ProcessHandlers(ctrl.CORSOptions, metrics.ActionOptions, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.ParseAcl(sa.be), ), ) @@ -1555,7 +1557,7 @@ func (sa *S3ApiRouter) Init() { sa.app.Options("/:bucket/*", middlewares.ApplyBucketCORSPreflightFallback(sa.be, sa.corsAllowOrigin), controllers.ProcessHandlers(ctrl.CORSOptions, metrics.ActionOptions, services, - middlewares.BucketObjectNameValidator(), + objNameValidator, middlewares.ParseAcl(sa.be), ), ) diff --git a/s3api/server.go b/s3api/server.go index 6ade20989..c78375bae 100644 --- a/s3api/server.go +++ b/s3api/server.go @@ -345,6 +345,14 @@ func WithDisableACL() Option { return func(s *S3ApiServer) { s.Router.disableACL = true } } +// WithDisableObjNameTraversalCheck disables the path-traversal rejection in +// object-name validation. Backends that store keys opaquely (not as filesystem +// paths) set this so keys like "../file.txt" are accepted and stored literally, +// matching AWS. The empty/"."/".."/"/" and control-character rejections stay on. +func WithDisableObjNameTraversalCheck() Option { + return func(s *S3ApiServer) { s.Router.disableObjNameTraversal = true } +} + // WithOnListen registers a callback that is invoked once the server is bound // and ready to accept requests. It is called from the fiber OnListen hook, // which fires immediately before the server begins serving the first diff --git a/s3api/utils/name_validate.go b/s3api/utils/name_validate.go index 5aabc874f..bef0d8120 100644 --- a/s3api/utils/name_validate.go +++ b/s3api/utils/name_validate.go @@ -15,11 +15,24 @@ package utils func IsObjectNameValid(name string) bool { + return IsObjectNameValidWithTraversal(name, true) +} + +// IsObjectNameValidWithTraversal validates an object key. The empty/"."/".."/"/" +// rejections always apply; the path-traversal (non-local) rejection applies only +// when checkTraversal is set. Backends that store keys as opaque strings rather +// than filesystem paths pass false, since a key like "../file.txt" is a legal +// literal S3 key for them and carries no traversal risk. +func IsObjectNameValidWithTraversal(name string, checkTraversal bool) bool { switch clean(name) { case "", ".", "..", "/": return false } + if !checkTraversal { + return true + } + return isObjectLocal(name) } From e30dd7f823c0543c78ce6eb8021964aee39a48c6 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 7 Sep 2026 16:47:00 +0100 Subject: [PATCH 06/15] revert: changes that cause AWS compatibility divergence --- .../bucket-object-name-validator.go | 12 +----- s3api/utils/name_validate.go | 12 ------ s3api/utils/name_validate_test.go | 41 ------------------- s3api/utils/precondition.go | 13 ++++-- s3api/utils/precondition_test.go | 15 +++---- s3err/s3err.go | 2 +- 6 files changed, 17 insertions(+), 78 deletions(-) delete mode 100644 s3api/utils/name_validate_test.go diff --git a/s3api/middlewares/bucket-object-name-validator.go b/s3api/middlewares/bucket-object-name-validator.go index 7e2dad46f..588e99b40 100644 --- a/s3api/middlewares/bucket-object-name-validator.go +++ b/s3api/middlewares/bucket-object-name-validator.go @@ -35,16 +35,8 @@ func BucketObjectNameValidator(checkTraversal bool) fiber.Handler { // check if the provided object name is valid // skip for empty objects: e.g bucket operations: HeadBucket... - if object != "" { - // A C1 control character in the key is unparseable to S3: reject - // pre-auth with InvalidURI, matching AWS. C0 controls (CR/LF/ESC) - // are legal and handled by the general validity check below. - if utils.ContainsC1ControlChar(object) { - return s3err.GetAPIError(s3err.ErrInvalidURI) - } - if !utils.IsObjectNameValidWithTraversal(object, checkTraversal) { - return s3err.GetAPIError(s3err.ErrBadRequest) - } + if object != "" && !utils.IsObjectNameValidWithTraversal(object, checkTraversal) { + return s3err.GetAPIError(s3err.ErrBadRequest) } return nil diff --git a/s3api/utils/name_validate.go b/s3api/utils/name_validate.go index bef0d8120..845783cbd 100644 --- a/s3api/utils/name_validate.go +++ b/s3api/utils/name_validate.go @@ -35,15 +35,3 @@ func IsObjectNameValidWithTraversal(name string, checkTraversal bool) bool { return isObjectLocal(name) } - -// ContainsC1ControlChar reports whether s contains a C1 control character -// (U+0080–U+009F). S3 rejects object keys containing these with InvalidURI; C0 -// controls (CR, LF, ESC, ...) are accepted and round-trip. -func ContainsC1ControlChar(s string) bool { - for _, r := range s { - if r >= 0x80 && r <= 0x9f { - return true - } - } - return false -} diff --git a/s3api/utils/name_validate_test.go b/s3api/utils/name_validate_test.go deleted file mode 100644 index 4c58e1166..000000000 --- a/s3api/utils/name_validate_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2025 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 utils_test - -import ( - "testing" - - "github.com/fil-forge/versitygw/s3api/utils" -) - -func TestContainsC1ControlChar(t *testing.T) { - cases := []struct { - name string - key string - want bool - }{ - {"c1 control U+008A (object-crud-0037 key)", "®Š-", true}, - {"crlf is c0, allowed", "test\r\nCRLF.txt", false}, - {"escape is c0, allowed", "testescape.txt", false}, - {"registered sign is above the c1 range", "®-plain", false}, - {"plain ascii", "hello/world.txt", false}, - {"empty", "", false}, - } - for _, c := range cases { - if got := utils.ContainsC1ControlChar(c.key); got != c.want { - t.Errorf("%s: ContainsC1ControlChar(%q) = %v, want %v", c.name, c.key, got, c.want) - } - } -} diff --git a/s3api/utils/precondition.go b/s3api/utils/precondition.go index db82c2393..1ee61a844 100644 --- a/s3api/utils/precondition.go +++ b/s3api/utils/precondition.go @@ -102,14 +102,19 @@ func ParsePreconditionDateHeader(date string) *time.Time { if date == "" { return nil } - // A future date is not ignored: S3 evaluates If-Modified-Since / - // If-Unmodified-Since against the object's last-modified time regardless - // (a future If-Modified-Since on an unmodified object yields 304). The - // comparison is EvaluatePreconditions' job, not the parser's. + // A date in the future is ignored: S3 does not evaluate a future + // If-Modified-Since / If-Unmodified-Since (verified against AWS S3 — a + // future If-Modified-Since on an unmodified object returns 200, not 304). if parsed, err := time.Parse(time.RFC1123, date); err == nil { + if parsed.After(time.Now()) { + return nil + } return &parsed } if parsed, err := time.Parse(time.RFC3339, date); err == nil { + if parsed.After(time.Now()) { + return nil + } return &parsed } return nil diff --git a/s3api/utils/precondition_test.go b/s3api/utils/precondition_test.go index 88e41406c..a01271f7f 100644 --- a/s3api/utils/precondition_test.go +++ b/s3api/utils/precondition_test.go @@ -30,15 +30,10 @@ func TestParsePreconditionDateHeader(t *testing.T) { t.Error("RFC3339 date parsed to nil, want a time") } - // A future date must be honored, not dropped: S3 evaluates - // If-Modified-Since against the object's last-modified time regardless of - // whether the date is in the future (a future If-Modified-Since on an - // unmodified object yields 304). See AWS S3 GetObject API reference. - future := ParsePreconditionDateHeader("Fri, 29 Oct 2100 19:43:31 GMT") - if future == nil { - t.Fatal("future RFC1123 date parsed to nil, want a time (future dates must not be ignored)") - } - if future.Year() != 2100 { - t.Errorf("future date year = %d, want 2100", future.Year()) + // A future date is dropped (returns nil): S3 does not evaluate a future + // If-Modified-Since / If-Unmodified-Since — verified against AWS S3, where a + // future If-Modified-Since on an unmodified object returns 200, not 304. + if got := ParsePreconditionDateHeader("Fri, 29 Oct 2100 19:43:31 GMT"); got != nil { + t.Errorf("future date = %v, want nil (future dates are ignored)", got) } } diff --git a/s3err/s3err.go b/s3err/s3err.go index 0c12c3062..68154cb4c 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -479,7 +479,7 @@ var errorCodeResponse = map[ErrorCode]APIError{ }, ErrInvalidURI: { Code: "InvalidURI", - Description: "Couldn't parse the specified URI.", + Description: "The specified URI couldn't be parsed.", HTTPStatusCode: http.StatusBadRequest, }, ErrObjectLockConfigurationNotFound: { From 9dd32bbd184eabe849ea1eaa34a5da73a9e21548 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 7 Sep 2026 20:08:32 +0100 Subject: [PATCH 07/15] fix: bucket name restrictions --- s3api/controllers/bucket-put.go | 10 +++++++ s3api/utils/utils.go | 26 +++++++++++++++++ s3api/utils/utils_test.go | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/s3api/controllers/bucket-put.go b/s3api/controllers/bucket-put.go index 9c4ac5115..8f866e9de 100644 --- a/s3api/controllers/bucket-put.go +++ b/s3api/controllers/bucket-put.go @@ -601,6 +601,16 @@ func (c S3ApiController) CreateBucket(ctx fiber.Ctx) (*Response, error) { }, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } + // a well-formed name reserved by AWS (sthree- prefix, -s3alias / --ol-s3 + // suffix) is rejected with AccessDenied rather than InvalidBucketName + if utils.IsReservedBucketName(bucket) { + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: bucketOwner.Access, + }, + }, s3err.GetAPIError(s3err.ErrAccessDenied) + } + // both bucket canned ACL and acl grants is not allowed if acl != "" && grants != "" { debuglogger.Logf("invalid request: %q (grants) %q (acl)", grants, acl) diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index 03cae3642..4722c29e0 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -344,9 +344,35 @@ func IsValidBucketName(bucket string) bool { debuglogger.Logf("bucket name is an ip address: %v\n", bucket) return false } + // Adjacent periods, a period next to a hyphen, and the xn-- prefix are + // rejected by AWS but not expressible in the character-class regexp above. + if strings.Contains(bucket, "..") || + strings.Contains(bucket, ".-") || + strings.Contains(bucket, "-.") { + debuglogger.Logf("bucket name has adjacent period/hyphen: %v\n", bucket) + return false + } + if strings.HasPrefix(bucket, "xn--") { + debuglogger.Logf("bucket name uses the reserved xn-- prefix: %v\n", bucket) + return false + } return true } +// IsReservedBucketName reports whether a well-formed bucket name uses a prefix +// or suffix AWS reserves for its own infrastructure (the S3 control plane, +// access point aliases, Object Lambda). CreateBucket on these is rejected with +// AccessDenied rather than InvalidBucketName: the name is valid but not one a +// caller may take. Follows the strict-validation switch, like IsValidBucketName. +func IsReservedBucketName(bucket string) bool { + if !strictBucketNameValidation.Load() { + return false + } + return strings.HasPrefix(bucket, "sthree-") || + strings.HasSuffix(bucket, "-s3alias") || + strings.HasSuffix(bucket, "--ol-s3") +} + func includeHeader(hdr string, signedHdrs []string) bool { return slices.ContainsFunc(signedHdrs, func(shdr string) bool { return strings.EqualFold(hdr, shdr) diff --git a/s3api/utils/utils_test.go b/s3api/utils/utils_test.go index 9fc969c95..ebac61fae 100644 --- a/s3api/utils/utils_test.go +++ b/s3api/utils/utils_test.go @@ -316,6 +316,34 @@ func TestIsValidBucketName(t *testing.T) { }, want: false, }, + { + name: "IsValidBucketName-adjacent-periods", + args: args{ + bucket: "my..bucket", + }, + want: false, + }, + { + name: "IsValidBucketName-period-hyphen", + args: args{ + bucket: "my.-bucket", + }, + want: false, + }, + { + name: "IsValidBucketName-hyphen-period", + args: args{ + bucket: "my-.bucket", + }, + want: false, + }, + { + name: "IsValidBucketName-xn--prefix", + args: args{ + bucket: "xn--bucket", + }, + want: false, + }, { name: "IsValidBucketName-valid-bucket-name", args: args{ @@ -333,6 +361,28 @@ func TestIsValidBucketName(t *testing.T) { } } +func TestIsReservedBucketName(t *testing.T) { + tests := []struct { + name string + bucket string + want bool + }{ + {"sthree-prefix", "sthree-mybucket", true}, + {"sthree-configurator", "sthree-configurator-x", true}, + {"s3alias-suffix", "mybucket-s3alias", true}, + {"ol-s3-suffix", "mybucket--ol-s3", true}, + {"ordinary-name", "my-bucket", false}, + {"contains-not-prefix-suffix", "my-sthree-bucket", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsReservedBucketName(tt.bucket); got != tt.want { + t.Errorf("IsReservedBucketName(%q) = %v, want %v", tt.bucket, got, tt.want) + } + }) + } +} + func TestSetBucketNameValidationStrict(t *testing.T) { SetBucketNameValidationStrict(true) t.Cleanup(func() { From b1dde87cdab70a641b168c3774ea9e0fe9a295ef Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 8 Sep 2026 00:27:32 +0100 Subject: [PATCH 08/15] fix: ListObjectsV2 rejects a present-but-empty continuation-token --- s3api/controllers/bucket-get.go | 12 ++++++++++++ s3api/controllers/bucket-get_test.go | 17 +++++++++++++++++ s3err/invalid-argument.go | 5 +++++ 3 files changed, 34 insertions(+) diff --git a/s3api/controllers/bucket-get.go b/s3api/controllers/bucket-get.go index 49c1ea3aa..cd3cfc544 100644 --- a/s3api/controllers/bucket-get.go +++ b/s3api/controllers/bucket-get.go @@ -21,6 +21,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/fil-forge/versitygw/auth" "github.com/fil-forge/versitygw/s3api/utils" + "github.com/fil-forge/versitygw/s3err" "github.com/fil-forge/versitygw/s3response" "github.com/gofiber/fiber/v3" ) @@ -595,6 +596,17 @@ func (c S3ApiController) ListObjectsV2(ctx fiber.Ctx) (*Response, error) { }, err } + // A present-but-empty continuation-token is invalid, not absent: AWS rejects + // it with InvalidArgument. ctx.Query collapses "absent" and "present, empty", + // so check the raw query args for the key's presence. + if cToken == "" && ctx.Request().URI().QueryArgs().Has("continuation-token") { + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, "") + } + res, err := c.be.ListObjectsV2(ctx.RequestCtx(), &s3.ListObjectsV2Input{ Bucket: &bucket, diff --git a/s3api/controllers/bucket-get_test.go b/s3api/controllers/bucket-get_test.go index 496bc2bf1..9523730b8 100644 --- a/s3api/controllers/bucket-get_test.go +++ b/s3api/controllers/bucket-get_test.go @@ -1114,6 +1114,23 @@ func TestS3ApiController_ListObjectsV2(t *testing.T) { err: s3err.GetAPIError(s3err.ErrNoSuchBucket), }, }, + { + name: "empty continuation token", + input: testInput{ + locals: defaultLocals, + queries: map[string]string{ + "continuation-token": "", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, ""), + }, + }, { name: "successful response", input: testInput{ diff --git a/s3err/invalid-argument.go b/s3err/invalid-argument.go index 3171514e3..f62899fbe 100644 --- a/s3err/invalid-argument.go +++ b/s3err/invalid-argument.go @@ -60,6 +60,7 @@ const ( InvalidArgDateHeader InvalidArgIndexDocumentSuffix InvalidArgErrorDocumentKey + InvalidArgContinuationToken ) var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{ @@ -207,6 +208,10 @@ var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{ Description: "The ErrorDocument Key is not well formed", ArgumentName: "ErrorDocument", }, + InvalidArgContinuationToken: { + Description: "The continuation token provided is incorrect", + ArgumentName: "continuation-token", + }, } // InvalidArgumentError is returned when a request argument is invalid. From c7be2f668a45ea226926db9bbe430213aac8fe98 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 8 Sep 2026 08:19:59 +0100 Subject: [PATCH 09/15] fix: url encoded keys, multipart complete empty body response fix, delete obj precondition, malformed auth header --- backend/common.go | 4 ++- s3api/controllers/bucket-get.go | 43 +++++++++++++++++++++++++++++ s3api/controllers/object-post.go | 11 ++++++++ s3api/middlewares/authentication.go | 9 ++++++ s3api/utils/auth-reader.go | 7 ++++- s3api/utils/name_validate.go | 10 +++++-- s3response/s3response.go | 12 +++++--- 7 files changed, 88 insertions(+), 8 deletions(-) diff --git a/backend/common.go b/backend/common.go index 84ef0c4c0..8907be2eb 100644 --- a/backend/common.go +++ b/backend/common.go @@ -770,7 +770,9 @@ type ObjectDeletePreconditions struct { func EvaluateObjectDeletePreconditions(etag string, modTime time.Time, size int64, preconditions ObjectDeletePreconditions) error { etag = strings.Trim(etag, `"`) ifMatch := preconditions.IfMatch - if ifMatch != nil && *ifMatch != etag { + // If-Match "*" matches any existing object; preconditions are only evaluated + // once the target object is known to exist, so "*" always passes here. + if ifMatch != nil && *ifMatch != "*" && *ifMatch != etag { return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatch) } diff --git a/s3api/controllers/bucket-get.go b/s3api/controllers/bucket-get.go index cd3cfc544..14cf7a90c 100644 --- a/s3api/controllers/bucket-get.go +++ b/s3api/controllers/bucket-get.go @@ -15,6 +15,7 @@ package controllers import ( + "net/url" "strings" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -550,6 +551,21 @@ func (c S3ApiController) ListMultipartUploads(ctx fiber.Ctx) (*Response, error) }, err } +// urlEncodeListKey applies EncodingType=url encoding to a list-response key or +// prefix: form-style escaping (space -> "+", "+" -> "%2B", ...) with "/" left +// intact, matching AWS. +func urlEncodeListKey(s string) string { + return strings.ReplaceAll(url.QueryEscape(s), "%2F", "/") +} + +func encStrPtr(p *string) *string { + if p == nil { + return nil + } + v := urlEncodeListKey(*p) + return &v +} + func (c S3ApiController) ListObjectsV2(ctx fiber.Ctx) (*Response, error) { // url values bucket := ctx.Params("bucket") @@ -625,6 +641,19 @@ func (c S3ApiController) ListObjectsV2(ctx fiber.Ctx) (*Response, error) { }, err } + if strings.EqualFold(ctx.Query("encoding-type"), "url") { + res.EncodingType = types.EncodingTypeUrl + res.Prefix = encStrPtr(res.Prefix) + res.Delimiter = encStrPtr(res.Delimiter) + res.StartAfter = encStrPtr(res.StartAfter) + for i := range res.Contents { + res.Contents[i].Key = encStrPtr(res.Contents[i].Key) + } + for i := range res.CommonPrefixes { + res.CommonPrefixes[i].Prefix = encStrPtr(res.CommonPrefixes[i].Prefix) + } + } + return &Response{ Headers: map[string]*string{ "x-amz-bucket-region": ®ion, @@ -697,6 +726,20 @@ func (c S3ApiController) ListObjects(ctx fiber.Ctx) (*Response, error) { }, err } + if strings.EqualFold(ctx.Query("encoding-type"), "url") { + res.EncodingType = types.EncodingTypeUrl + res.Prefix = encStrPtr(res.Prefix) + res.Delimiter = encStrPtr(res.Delimiter) + res.Marker = encStrPtr(res.Marker) + res.NextMarker = encStrPtr(res.NextMarker) + for i := range res.Contents { + res.Contents[i].Key = encStrPtr(res.Contents[i].Key) + } + for i := range res.CommonPrefixes { + res.CommonPrefixes[i].Prefix = encStrPtr(res.CommonPrefixes[i].Prefix) + } + } + return &Response{ Headers: map[string]*string{ "x-amz-bucket-region": ®ion, diff --git a/s3api/controllers/object-post.go b/s3api/controllers/object-post.go index f48ad5a88..1110fc092 100644 --- a/s3api/controllers/object-post.go +++ b/s3api/controllers/object-post.go @@ -299,6 +299,17 @@ func (c S3ApiController) CompleteMultipartUpload(ctx fiber.Ctx) (*Response, erro }, err } + // An empty request body is rejected as InvalidRequest (AWS), distinct from + // the MalformedXML returned for a present-but-unparseable body. + if len(ctx.BodyRaw()) == 0 { + debuglogger.Logf("empty body provided for complete multipart upload") + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + var body s3response.CompleteMultipartUploadRequestBody err = xml.Unmarshal(ctx.BodyRaw(), &body) if err != nil { diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index d2a114787..e0dde3bf8 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -51,6 +51,15 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, return nil } + // A present but malformed Authorization header is rejected before the + // date check so it surfaces as InvalidArgument (AWS) rather than the + // missing-date AccessDenied. + if hdr := ctx.Get("Authorization"); hdr != "" { + if _, err := utils.ParseAuthorization(hdr); err != nil { + return err + } + } + // Check X-Amz-Date header date := ctx.Get("X-Amz-Date") if date == "" { diff --git a/s3api/utils/auth-reader.go b/s3api/utils/auth-reader.go index f6f61f213..ebc77acf4 100644 --- a/s3api/utils/auth-reader.go +++ b/s3api/utils/auth-reader.go @@ -138,7 +138,12 @@ func ParseAuthorization(authorization string) (AuthData, error) { algo := authParts[0] if algo == "AWS" { - // SigV2 authorization is not supported by the gateway + // SigV2 header form is "AWS :". A malformed value + // (missing the ":") is rejected as InvalidArgument, matching AWS; a + // well-formed SigV2 header is unsupported by the gateway. + if !strings.Contains(authParts[1], ":") { + return a, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthHeader, authorization) + } return a, s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationMechanism) } if algo != "AWS4-HMAC-SHA256" { diff --git a/s3api/utils/name_validate.go b/s3api/utils/name_validate.go index 845783cbd..ef32e5cec 100644 --- a/s3api/utils/name_validate.go +++ b/s3api/utils/name_validate.go @@ -24,14 +24,20 @@ func IsObjectNameValid(name string) bool { // than filesystem paths pass false, since a key like "../file.txt" is a legal // literal S3 key for them and carries no traversal risk. func IsObjectNameValidWithTraversal(name string, checkTraversal bool) bool { - switch clean(name) { - case "", ".", "..", "/": + if name == "" { return false } + // Opaque backends store keys as literal strings, so any non-empty key is + // valid — AWS accepts "/", "//", ".", ".." and the like as object keys. if !checkTraversal { return true } + switch clean(name) { + case "", ".", "..", "/": + return false + } + return isObjectLocal(name) } diff --git a/s3response/s3response.go b/s3response/s3response.go index ca0f7f1e2..9ca643642 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -135,10 +135,14 @@ type GetObjectAttributesResponse struct { } type ObjectParts struct { - PartNumberMarker int - NextPartNumberMarker int - MaxParts int - IsTruncated bool + // PartsCount is the object's total multipart part count (AWS , + // the SDK's TotalPartsCount). Emitted for every completed multipart object; + // the per-part list below is populated only when parts carry checksums. + PartsCount int `xml:"PartsCount,omitempty"` + PartNumberMarker int `xml:",omitempty"` + NextPartNumberMarker int `xml:",omitempty"` + MaxParts int `xml:",omitempty"` + IsTruncated bool `xml:",omitempty"` Parts []types.ObjectPart `xml:"Part"` } From cae1b9169d1d05b44d43ad746f49f574238bd5ab Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 8 Sep 2026 09:14:07 +0100 Subject: [PATCH 10/15] fix: object parts fields for non-checksummed output --- backend/s3proxy/s3.go | 15 +++++++-------- s3response/s3response.go | 15 +++++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go index 4b0ef4319..a19557a35 100644 --- a/backend/s3proxy/s3.go +++ b/backend/s3proxy/s3.go @@ -1184,21 +1184,20 @@ func (s *S3Proxy) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAt objParts := out.ObjectParts if objParts != nil { if objParts.PartNumberMarker != nil { - partNumberMarker, err := strconv.Atoi(*objParts.PartNumberMarker) - if err != nil { - parts.PartNumberMarker = partNumberMarker + if partNumberMarker, err := strconv.Atoi(*objParts.PartNumberMarker); err == nil { + parts.PartNumberMarker = &partNumberMarker } if objParts.NextPartNumberMarker != nil { - nextPartNumberMarker, err := strconv.Atoi(*objParts.NextPartNumberMarker) - if err != nil { - parts.NextPartNumberMarker = nextPartNumberMarker + if nextPartNumberMarker, err := strconv.Atoi(*objParts.NextPartNumberMarker); err == nil { + parts.NextPartNumberMarker = &nextPartNumberMarker } } if objParts.IsTruncated != nil { - parts.IsTruncated = *objParts.IsTruncated + parts.IsTruncated = objParts.IsTruncated } if objParts.MaxParts != nil { - parts.MaxParts = int(*objParts.MaxParts) + mp := int(*objParts.MaxParts) + parts.MaxParts = &mp } parts.Parts = objParts.Parts } diff --git a/s3response/s3response.go b/s3response/s3response.go index 9ca643642..ed70f7781 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -136,13 +136,16 @@ type GetObjectAttributesResponse struct { type ObjectParts struct { // PartsCount is the object's total multipart part count (AWS , - // the SDK's TotalPartsCount). Emitted for every completed multipart object; - // the per-part list below is populated only when parts carry checksums. + // the SDK's TotalPartsCount). Emitted for every completed multipart object. + // The pagination fields and per-part list below are populated only when the + // parts carry checksums; they are pointers so a checksummed object emits the + // zero values (PartNumberMarker 0, IsTruncated false) while a non-checksummed + // object omits them entirely, matching AWS in both cases. PartsCount int `xml:"PartsCount,omitempty"` - PartNumberMarker int `xml:",omitempty"` - NextPartNumberMarker int `xml:",omitempty"` - MaxParts int `xml:",omitempty"` - IsTruncated bool `xml:",omitempty"` + PartNumberMarker *int `xml:"PartNumberMarker,omitempty"` + NextPartNumberMarker *int `xml:"NextPartNumberMarker,omitempty"` + MaxParts *int `xml:"MaxParts,omitempty"` + IsTruncated *bool `xml:"IsTruncated,omitempty"` Parts []types.ObjectPart `xml:"Part"` } From 875fc5509768307015c84e7ded07362383626900 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 8 Sep 2026 11:52:12 +0100 Subject: [PATCH 11/15] fix: populate ACL properties of create bucket input --- s3api/controllers/bucket-put.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/s3api/controllers/bucket-put.go b/s3api/controllers/bucket-put.go index 8f866e9de..c6b09aa8c 100644 --- a/s3api/controllers/bucket-put.go +++ b/s3api/controllers/bucket-put.go @@ -718,6 +718,14 @@ func (c S3ApiController) CreateBucket(ctx fiber.Ctx) (*Response, error) { Bucket: &bucket, ObjectOwnership: objectOwnership, ObjectLockEnabledForBucket: &lockEnabled, + // Forward the requested ACL so backends that do not model ACLs can + // reject a non-default request; backends that store ACLs use updAcl. + ACL: acl, + GrantFullControl: &grantFullControl, + GrantRead: &grantRead, + GrantReadACP: &grantReadACP, + GrantWrite: &grantWrite, + GrantWriteACP: &grantWriteACP, CreateBucketConfiguration: &types.CreateBucketConfiguration{ Tags: body.TagSet, }, From 7fbc1d13dcd03570874d9f4b27ad509be0f59ca3 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 8 Sep 2026 14:00:29 +0100 Subject: [PATCH 12/15] fix: populate acl and grant fields for write commands --- s3api/controllers/bucket-put.go | 10 +++++----- s3api/controllers/object-post.go | 17 +++++++++++++++-- s3api/controllers/object-put.go | 29 +++++++++++++++++++++++++---- s3response/s3response.go | 1 + 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/s3api/controllers/bucket-put.go b/s3api/controllers/bucket-put.go index c6b09aa8c..0efa66370 100644 --- a/s3api/controllers/bucket-put.go +++ b/s3api/controllers/bucket-put.go @@ -721,11 +721,11 @@ func (c S3ApiController) CreateBucket(ctx fiber.Ctx) (*Response, error) { // Forward the requested ACL so backends that do not model ACLs can // reject a non-default request; backends that store ACLs use updAcl. ACL: acl, - GrantFullControl: &grantFullControl, - GrantRead: &grantRead, - GrantReadACP: &grantReadACP, - GrantWrite: &grantWrite, - GrantWriteACP: &grantWriteACP, + GrantFullControl: utils.GetStringPtr(grantFullControl), + GrantRead: utils.GetStringPtr(grantRead), + GrantReadACP: utils.GetStringPtr(grantReadACP), + GrantWrite: utils.GetStringPtr(grantWrite), + GrantWriteACP: utils.GetStringPtr(grantWriteACP), CreateBucketConfiguration: &types.CreateBucketConfiguration{ Tags: body.TagSet, }, diff --git a/s3api/controllers/object-post.go b/s3api/controllers/object-post.go index 1110fc092..2b0d465d5 100644 --- a/s3api/controllers/object-post.go +++ b/s3api/controllers/object-post.go @@ -231,10 +231,23 @@ func (c S3ApiController) CreateMultipartUpload(ctx fiber.Ctx) (*Response, error) }, err } + acl := types.ObjectCannedACL(c.getAclHeaderValue(ctx, "X-Amz-Acl")) + grantFullControl := c.getAclHeaderValue(ctx, "X-Amz-Grant-Full-Control") + grantRead := c.getAclHeaderValue(ctx, "X-Amz-Grant-Read") + grantReadACP := c.getAclHeaderValue(ctx, "X-Amz-Grant-Read-Acp") + grantWriteACP := c.getAclHeaderValue(ctx, "X-Amz-Grant-Write-Acp") + res, err := c.be.CreateMultipartUpload(ctx.RequestCtx(), s3response.CreateMultipartUploadInput{ - Bucket: &bucket, - Key: &key, + Bucket: &bucket, + Key: &key, + // Forward the requested ACL so backends that do not model ACLs can + // reject a request that sets one; backends that store ACLs use it. + ACL: acl, + GrantFullControl: utils.GetStringPtr(grantFullControl), + GrantRead: utils.GetStringPtr(grantRead), + GrantReadACP: utils.GetStringPtr(grantReadACP), + GrantWriteACP: utils.GetStringPtr(grantWriteACP), Tagging: &tagging, ContentType: &contentType, ContentEncoding: &contentEncoding, diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go index 88603b29b..044750aec 100644 --- a/s3api/controllers/object-put.go +++ b/s3api/controllers/object-put.go @@ -620,8 +620,16 @@ func (c S3ApiController) CopyObject(ctx fiber.Ctx) (*Response, error) { res, err := c.be.CopyObject(ctx.RequestCtx(), s3response.CopyObjectInput{ - Bucket: &bucket, - Key: &key, + Bucket: &bucket, + Key: &key, + // Forward the requested ACL so backends that do not model ACLs can + // reject a request that sets one. Absent grant headers stay nil + // (GetStringPtr) to distinguish "unset" from an explicit empty value. + ACL: types.ObjectCannedACL(c.getAclHeaderValue(ctx, "X-Amz-Acl")), + GrantFullControl: utils.GetStringPtr(c.getAclHeaderValue(ctx, "X-Amz-Grant-Full-Control")), + GrantRead: utils.GetStringPtr(c.getAclHeaderValue(ctx, "X-Amz-Grant-Read")), + GrantReadACP: utils.GetStringPtr(c.getAclHeaderValue(ctx, "X-Amz-Grant-Read-Acp")), + GrantWriteACP: utils.GetStringPtr(c.getAclHeaderValue(ctx, "X-Amz-Grant-Write-Acp")), ContentType: &contentType, ContentDisposition: &contentDisposition, ContentEncoding: &contentEncoding, @@ -797,10 +805,23 @@ func (c S3ApiController) PutObject(ctx fiber.Ctx) (*Response, error) { ifMatch, ifNoneMatch := utils.ParsePreconditionMatchHeaders(ctx) + acl := types.ObjectCannedACL(c.getAclHeaderValue(ctx, "X-Amz-Acl")) + grantFullControl := c.getAclHeaderValue(ctx, "X-Amz-Grant-Full-Control") + grantRead := c.getAclHeaderValue(ctx, "X-Amz-Grant-Read") + grantReadACP := c.getAclHeaderValue(ctx, "X-Amz-Grant-Read-Acp") + grantWriteACP := c.getAclHeaderValue(ctx, "X-Amz-Grant-Write-Acp") + res, err := c.be.PutObject(ctx.RequestCtx(), s3response.PutObjectInput{ - Bucket: &bucket, - Key: &key, + Bucket: &bucket, + Key: &key, + // Forward the requested ACL so backends that do not model ACLs can + // reject a request that sets one; backends that store ACLs use it. + ACL: acl, + GrantFullControl: utils.GetStringPtr(grantFullControl), + GrantRead: utils.GetStringPtr(grantRead), + GrantReadACP: utils.GetStringPtr(grantReadACP), + GrantWriteACP: utils.GetStringPtr(grantWriteACP), ContentLength: &contentLength, ContentType: &contentType, ContentEncoding: &contentEncoding, diff --git a/s3response/s3response.go b/s3response/s3response.go index ed70f7781..3d4d66420 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -600,6 +600,7 @@ type PutObjectInput struct { ChecksumXXHASH128 *string ContentMD5 *string ExpectedBucketOwner *string + ACL types.ObjectCannedACL GrantFullControl *string GrantRead *string GrantReadACP *string From 9c26479c76639173d49e7a917ea3344b837f3d1f Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 10 Sep 2026 12:31:08 +0100 Subject: [PATCH 13/15] fix: PutObject versioning suspended x-amz-version-id header --- tests/integration/ListObjectVersions.go | 8 +++-- tests/integration/versioning.go | 45 ++++++++++++++++++++----- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/tests/integration/ListObjectVersions.go b/tests/integration/ListObjectVersions.go index 0f04cee02..9fb59fa7e 100644 --- a/tests/integration/ListObjectVersions.go +++ b/tests/integration/ListObjectVersions.go @@ -336,9 +336,11 @@ func ListObjectVersions_containing_null_versionId_obj(s *S3Conf) error { return err } - if getString(out.res.VersionId) != nullVersionId { - return fmt.Errorf("expected the uploaded object versionId to be %v, instead got %v", - nullVersionId, getString(out.res.VersionId)) + // A suspended bucket stores the object as the "null" version but the + // PutObject response carries no x-amz-version-id header. + if out.res.VersionId != nil { + return fmt.Errorf("expected no versionId in the suspended bucket PutObject response, instead got %v", + *out.res.VersionId) } versions[0].IsLatest = getBoolPtr(false) diff --git a/tests/integration/versioning.go b/tests/integration/versioning.go index d3e08cc13..d4cc81cdf 100644 --- a/tests/integration/versioning.go +++ b/tests/integration/versioning.go @@ -55,8 +55,8 @@ func Versioning_DeleteBucket_not_empty(s *S3Conf) error { func Versioning_PutObject_suspended_null_versionId_obj(s *S3Conf) error { testName := "Versioning_PutObject_suspended_null_versionId_obj" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { - obj := "my-obj" - out, err := putObjectWithData(1222, &s3.PutObjectInput{ + obj, lgth := "my-obj", int64(1222) + out, err := putObjectWithData(lgth, &s3.PutObjectInput{ Bucket: &bucket, Key: &obj, }, s3client) @@ -64,9 +64,36 @@ func Versioning_PutObject_suspended_null_versionId_obj(s *S3Conf) error { return err } - if getString(out.res.VersionId) != nullVersionId { - return fmt.Errorf("expected the uploaded object versionId to be %v, instead got %v", - nullVersionId, getString(out.res.VersionId)) + // A suspended bucket stores the object as the "null" version but the + // PutObject response carries no x-amz-version-id header. + if out.res.VersionId != nil { + return fmt.Errorf("expected no versionId in the suspended bucket PutObject response, instead got %v", + *out.res.VersionId) + } + + versions := []types.ObjectVersion{ + { + ETag: out.res.ETag, + IsLatest: getBoolPtr(true), + Key: &obj, + Size: &lgth, + VersionId: &nullVersionId, + StorageClass: types.ObjectVersionStorageClassStandard, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ + Bucket: &bucket, + }) + cancel() + if err != nil { + return err + } + + if !compareVersions(versions, res.Versions) { + return fmt.Errorf("expected the listed versions to be %v, instead got %v", + versions, res.Versions) } return nil @@ -161,9 +188,11 @@ func Versioning_PutObject_overwrite_null_versionId_obj(s *S3Conf) error { return err } - if getString(out.res.VersionId) != nullVersionId { - return fmt.Errorf("expected the uploaded object versionId to be %v, insted got %v", - nullVersionId, getString(out.res.VersionId)) + // A suspended bucket stores the object as the "null" version but the + // PutObject response carries no x-amz-version-id header. + if out.res.VersionId != nil { + return fmt.Errorf("expected no versionId in the suspended bucket PutObject response, instead got %v", + *out.res.VersionId) } versions[0].IsLatest = getBoolPtr(false) From 85d0c0e95395f40db7f84ea2f4f8d6d1e9c84e74 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 11 Sep 2026 09:30:18 +0100 Subject: [PATCH 14/15] fix: integration tests --- s3api/utils/auth-reader.go | 2 +- s3api/utils/auth_test.go | 28 ++++++++++++++++++++++++++++ s3err/invalid-argument.go | 5 +++++ tests/integration/group-tests.go | 2 ++ tests/integration/sigv4_auth.go | 23 ++++++++++++++++++++++- 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/s3api/utils/auth-reader.go b/s3api/utils/auth-reader.go index ebc77acf4..becb88e34 100644 --- a/s3api/utils/auth-reader.go +++ b/s3api/utils/auth-reader.go @@ -142,7 +142,7 @@ func ParseAuthorization(authorization string) (AuthData, error) { // (missing the ":") is rejected as InvalidArgument, matching AWS; a // well-formed SigV2 header is unsupported by the gateway. if !strings.Contains(authParts[1], ":") { - return a, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthHeader, authorization) + return a, s3err.GetInvalidArgumentErr(s3err.InvalidArgSigV2AuthHeader, authorization) } return a, s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationMechanism) } diff --git a/s3api/utils/auth_test.go b/s3api/utils/auth_test.go index 5a8463194..118856d5d 100644 --- a/s3api/utils/auth_test.go +++ b/s3api/utils/auth_test.go @@ -16,11 +16,13 @@ package utils import ( "net" + "reflect" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" v4 "github.com/fil-forge/versitygw/aws/signer/v4" + "github.com/fil-forge/versitygw/s3err" "github.com/gofiber/fiber/v3" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttputil" @@ -151,3 +153,29 @@ func Test_Client_UserAgent(t *testing.T) { t.Fatal(err) } } + +func TestAuthParseSigV2(t *testing.T) { + for _, tc := range []struct { + name string + authstr string + want error + }{ + { + name: "well-formed sigv2 is unsupported", + authstr: "AWS AKIAIOSFODNN7EXAMPLE:frJIUN8DYpKDtOLCwo//yllqDzg=", + want: s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationMechanism), + }, + { + name: "sigv2 without key:signature is a malformed header", + authstr: "AWS seed_signature", + want: s3err.GetInvalidArgumentErr(s3err.InvalidArgSigV2AuthHeader, "AWS seed_signature"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := ParseAuthorization(tc.authstr) + if !reflect.DeepEqual(err, tc.want) { + t.Errorf("got %#v, want %#v", err, tc.want) + } + }) + } +} diff --git a/s3err/invalid-argument.go b/s3err/invalid-argument.go index f62899fbe..218816162 100644 --- a/s3err/invalid-argument.go +++ b/s3err/invalid-argument.go @@ -35,6 +35,7 @@ const ( InvalidArgCopySourceEncoding InvalidArgURLEncodedTagging InvalidArgAuthHeader + InvalidArgSigV2AuthHeader InvalidArgAuthorizationType InvalidArgPOSTFileRequired InvalidArgSHA256Payload @@ -108,6 +109,10 @@ var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{ Description: "Authorization header is invalid -- one and only one ' ' (space) required.", ArgumentName: "Authorization", }, + InvalidArgSigV2AuthHeader: { + Description: "AWS authorization header is invalid. Expected AwsAccessKeyId:signature", + ArgumentName: "Authorization", + }, InvalidArgAuthorizationType: { Description: "Unsupported Authorization Type", ArgumentName: "Authorization", diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index ddfbae1d5..276218333 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -40,6 +40,7 @@ func TestAuthentication(ts *TestState) { ts.Run(Authentication_unsigned_non_required_header) ts.Run(Authentication_signature_error_incorrect_secret_key) ts.Run(Authentication_sigv2_not_supported) + ts.Run(Authentication_sigv2_malformed) ts.Run(Authentication_with_expect_header) } @@ -1386,6 +1387,7 @@ func GetIntTests() IntTests { "Authentication_unsigned_non_required_header": Authentication_unsigned_non_required_header, "Authentication_signature_error_incorrect_secret_key": Authentication_signature_error_incorrect_secret_key, "Authentication_sigv2_not_supported": Authentication_sigv2_not_supported, + "Authentication_sigv2_malformed": Authentication_sigv2_malformed, "Authentication_with_expect_header": Authentication_with_expect_header, "PresignedAuth_security_token_not_supported": PresignedAuth_security_token_not_supported, "PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm, diff --git a/tests/integration/sigv4_auth.go b/tests/integration/sigv4_auth.go index efeef1b57..7d87723bf 100644 --- a/tests/integration/sigv4_auth.go +++ b/tests/integration/sigv4_auth.go @@ -580,7 +580,7 @@ func Authentication_sigv2_not_supported(s *S3Conf) error { } req.Header.Del("Authorization") - req.Header.Set("Authorization", "AWS seed_signature") + req.Header.Set("Authorization", "AWS AKIAIOSFODNN7EXAMPLE:frJIUN8DYpKDtOLCwo//yllqDzg=") resp, err := s.httpClient.Do(req) if err != nil { @@ -595,6 +595,27 @@ func Authentication_sigv2_not_supported(s *S3Conf) error { }) } +func Authentication_sigv2_malformed(s *S3Conf) error { + testName := "Authentication_sigv2_malformed" + return authHandler(s, &authConfig{ + testName: testName, + method: http.MethodGet, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + // SigV2 is "AWS :"; without the ":" AWS rejects + // the header itself rather than the signature version + req.Header.Set("Authorization", "AWS seed_signature") + + resp, err := s.httpClient.Do(req) + if err != nil { + return err + } + + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgSigV2AuthHeader, "AWS seed_signature")) + }) +} + func Authentication_with_expect_header(s *S3Conf) error { testName := "Authentication_with_expect_header" bucket, object := getBucketName(), "object" From b2b05369bf37c7addfb9c71f8fd1c6182e4542fe Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 11 Sep 2026 10:31:57 +0100 Subject: [PATCH 15/15] fix: functional tests --- s3api/controllers/object-put.go | 17 ++++++++- s3api/controllers/object-put_test.go | 53 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go index 044750aec..b61d29dda 100644 --- a/s3api/controllers/object-put.go +++ b/s3api/controllers/object-put.go @@ -864,7 +864,7 @@ func (c S3ApiController) PutObject(ctx fiber.Ctx) (*Response, error) { "x-amz-checksum-xxhash3": res.ChecksumXXHASH3, "x-amz-checksum-xxhash128": res.ChecksumXXHASH128, "x-amz-checksum-type": utils.ConvertToStringPtr(res.ChecksumType), - "x-amz-version-id": &res.VersionID, + "x-amz-version-id": putObjectVersionIdHeader(res.VersionID), "x-amz-object-size": utils.ConvertPtrToStringPtr(res.Size), }, MetaOpts: &MetaOptions{ @@ -876,3 +876,18 @@ func (c S3ApiController) PutObject(ctx fiber.Ctx) (*Response, error) { }, }, err } + +// nullVersionId is the version id of an object written while bucket versioning +// is off or suspended. +const nullVersionId = "null" + +// putObjectVersionIdHeader returns the x-amz-version-id value for a PutObject +// response. AWS only sends the header for a version it generated: a PUT into a +// bucket with versioning suspended stores the "null" version and the response +// carries no x-amz-version-id at all. +func putObjectVersionIdHeader(versionId string) *string { + if versionId == nullVersionId { + return nil + } + return &versionId +} diff --git a/s3api/controllers/object-put_test.go b/s3api/controllers/object-put_test.go index dd20e5e45..abd41bbc4 100644 --- a/s3api/controllers/object-put_test.go +++ b/s3api/controllers/object-put_test.go @@ -1303,6 +1303,59 @@ func TestS3ApiController_PutObject(t *testing.T) { }, }, }, + { + name: "suspended bucket null version omits x-amz-version-id", + input: testInput{ + locals: map[utils.ContextKey]any{ + utils.ContextKeyIsRoot: true, + utils.ContextKeyParsedAcl: auth.ACL{ + Owner: "root", + }, + utils.ContextKeyAccount: auth.Account{ + Access: "root", + Role: auth.RoleAdmin, + }, + utils.ContextKeyBodyReader: strings.NewReader("something"), + }, + extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound), + headers: map[string]string{ + "Content-Length": "3", + }, + body: []byte("aaa"), + beRes: s3response.PutObjectOutput{ + ETag: "ETag", + VersionID: "null", + Size: &objSize, + }, + }, + output: testOutput{ + response: &Response{ + Headers: map[string]*string{ + "ETag": utils.GetStringPtr("ETag"), + "x-amz-checksum-crc32": nil, + "x-amz-checksum-crc32c": nil, + "x-amz-checksum-crc64nvme": nil, + "x-amz-checksum-sha1": nil, + "x-amz-checksum-sha256": nil, + "x-amz-checksum-sha512": nil, + "x-amz-checksum-md5": nil, + "x-amz-checksum-xxhash64": nil, + "x-amz-checksum-xxhash3": nil, + "x-amz-checksum-xxhash128": nil, + "x-amz-checksum-type": utils.GetStringPtr(""), + "x-amz-version-id": nil, + "x-amz-object-size": utils.ConvertToStringPtr(objSize), + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + ObjectETag: utils.GetStringPtr("ETag"), + EventName: s3event.EventObjectCreatedPut, + ContentLength: 3, + ObjectSize: 3, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {