diff --git a/s3api/controllers/bucket-get.go b/s3api/controllers/bucket-get.go index 4340a75b..d1393f53 100644 --- a/s3api/controllers/bucket-get.go +++ b/s3api/controllers/bucket-get.go @@ -22,6 +22,7 @@ import ( "github.com/gofiber/fiber/v3" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/s3api/utils" + "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3response" ) @@ -614,6 +615,15 @@ func (c S3ApiController) ListObjects(ctx fiber.Ctx) (*Response, error) { region = defaultRegion } + // start-after is a ListObjectsV2 argument, S3 rejects it on a V1 listing + if ctx.Query("start-after") != "" { + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, "") + } + err := c.verifyAccess(ctx, auth.AccessOptions{ Acl: parsedAcl, AclPermission: auth.PermissionRead, diff --git a/s3api/controllers/bucket-get_test.go b/s3api/controllers/bucket-get_test.go index ac290891..87701507 100644 --- a/s3api/controllers/bucket-get_test.go +++ b/s3api/controllers/bucket-get_test.go @@ -1195,6 +1195,24 @@ func TestS3ApiController_ListObjects(t *testing.T) { err: s3err.GetAPIError(s3err.ErrAccessDenied), }, }, + { + name: "start-after is not supported", + input: testInput{ + locals: defaultLocals, + beRes: listResult, + queries: map[string]string{ + "start-after": "my-key", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, ""), + }, + }, { name: "invalid max keys", input: testInput{ diff --git a/s3err/invalid-argument.go b/s3err/invalid-argument.go index 243027ce..cd22a057 100644 --- a/s3err/invalid-argument.go +++ b/s3err/invalid-argument.go @@ -26,6 +26,7 @@ type InvalidArgErrorCode int const ( InvalidArgMaxBuckets InvalidArgErrorCode = iota InvalidArgNegativeMaxKeys + InvalidArgStartAfter InvalidArgObjectAttributes InvalidArgPartNumber InvalidArgCompleteMpPartNumber @@ -90,6 +91,10 @@ var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{ Description: "max-keys cannot be negative", ArgumentName: "maxKeys", }, + InvalidArgStartAfter: { + Description: "startAfter only supported in REST.GET.BUCKET with list-type=2", + ArgumentName: "start-after", + }, InvalidArgObjectAttributes: { Description: "Invalid attribute name specified.", ArgumentName: "x-amz-object-attributes", diff --git a/tests/integration/ListObjects.go b/tests/integration/ListObjects.go index 31dfead9..59fdfe41 100644 --- a/tests/integration/ListObjects.go +++ b/tests/integration/ListObjects.go @@ -17,6 +17,8 @@ package integration import ( "context" "fmt" + "net/http" + "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" @@ -193,6 +195,33 @@ func ListObjects_invalid_max_keys(s *S3Conf) error { }) } +// start-after belongs to ListObjectsV2, so S3 rejects it on a V1 listing +// instead of ignoring it. The SDK ListObjects input has no StartAfter field, +// so the request is signed and sent by hand. +func ListObjects_start_after(s *S3Conf) error { + testName := "ListObjects_start_after" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + _, err := putObjects(s3client, []string{"bar", "baz", "foo"}, bucket) + if err != nil { + return err + } + + req, err := createSignedReq(http.MethodGet, s.endpoint, + fmt.Sprintf("%v?start-after=bar", bucket), + s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), nil) + if err != nil { + return err + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return err + } + + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, "")) + }) +} + func ListObjects_max_keys_0(s *S3Conf) error { testName := "ListObjects_max_keys_0" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index c0f82614..940ae164 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -314,6 +314,7 @@ func TestListObjects(ts *TestState) { ts.Run(ListObjects_truncated) ts.Run(ListObjects_paginated) ts.Run(ListObjects_invalid_max_keys) + ts.Run(ListObjects_start_after) ts.Run(ListObjects_max_keys_0) ts.Run(ListObjects_exceeding_max_keys) ts.Run(ListObjects_delimiter) @@ -3153,6 +3154,7 @@ func GetIntTests() IntTests { "ListObjects_truncated": ListObjects_truncated, "ListObjects_paginated": ListObjects_paginated, "ListObjects_invalid_max_keys": ListObjects_invalid_max_keys, + "ListObjects_start_after": ListObjects_start_after, "ListObjects_max_keys_0": ListObjects_max_keys_0, "ListObjects_delimiter": ListObjects_delimiter, "ListObjects_max_keys_none": ListObjects_max_keys_none, diff --git a/tests/test_rest_list_objects.sh b/tests/test_rest_list_objects.sh index 8f30c9be..7fe13ab6 100755 --- a/tests/test_rest_list_objects.sh +++ b/tests/test_rest_list_objects.sh @@ -248,9 +248,6 @@ source ./tests/drivers/objects_and_versions.sh # tags: curl,ListObjects,start-after,invalid-query @test "ListObjectsV1 - start-after - error" { - if [ "$DIRECT" != "true" ]; then - skip "https://github.com/versity/versitygw/issues/2004" - fi run setup_bucket_and_files_v3 "$BUCKET_ONE_NAME" 2 assert_success read -r bucket_name file_one file_two <<< "$output"