diff --git a/s3api/router.go b/s3api/router.go index 22450f9..60940d3 100644 --- a/s3api/router.go +++ b/s3api/router.go @@ -89,6 +89,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ } // ListBuckets action + + // copy source is not allowed on '/' + app.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + app.Get("/", controllers.ProcessHandlers( ctrl.ListBuckets, @@ -384,6 +390,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ )) // HeadBucket action + + // copy source is not allowed on bucket HEAD operation + bucketRouter.Head("/", middlewares.MatchHeader("X-Amz-Copy-Source"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + bucketRouter.Head("", controllers.ProcessHandlers( ctrl.HeadBucket, @@ -399,6 +411,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ )) // DELETE bucket operations + + // copy source is not allowed on bucket DELETE operation + bucketRouter.Delete("/", middlewares.MatchHeader("X-Amz-Copy-Source"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + bucketRouter.Delete("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessHandlers( @@ -582,6 +600,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ )) // GET bucket operations + + // copy source is not allowed on bucket GET operation + bucketRouter.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + bucketRouter.Get("", middlewares.MatchQueryArgs("location"), controllers.ProcessHandlers( @@ -973,6 +997,13 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ middlewares.ParseAcl(be), )) + // bucket POST operation is not allowed with uploadId and copy source + bucketRouter.Post("/", + middlewares.MatchHeader("X-Amz-Copy-Source"), + middlewares.MatchQueryArgs("uploadId"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + // DeleteObjects action bucketRouter.Post("", middlewares.MatchQueryArgs("delete"), @@ -989,6 +1020,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ middlewares.ParseAcl(be), )) + // object HEAD operation is not allowed with copy source + objectRouter.Head("/", + middlewares.MatchHeader("X-Amz-Copy-Source"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + // HeadObject objectRouter.Head("", controllers.ProcessHandlers( @@ -1011,6 +1048,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrGetUploadsWithKey)), metrics.ActionUndetected, services), ) + // object GET operation is not allowed with copy source + objectRouter.Get("/", + middlewares.MatchHeader("X-Amz-Copy-Source"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + objectRouter.Get("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessHandlers( @@ -1103,6 +1146,13 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ )) // DELETE object operations + + // object DELETE operation is not allowed with copy source + objectRouter.Delete("/", + middlewares.MatchHeader("X-Amz-Copy-Source"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + objectRouter.Delete("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessHandlers( @@ -1142,6 +1192,15 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ middlewares.ParseAcl(be), )) + // object POST operations + + // object POST operation is not allowed with copy source and uploadId + objectRouter.Post("/", + middlewares.MatchHeader("X-Amz-Copy-Source"), + middlewares.MatchQueryArgs("uploadId"), + controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + ) + objectRouter.Post("", middlewares.MatchQueryArgs("restore"), controllers.ProcessHandlers( diff --git a/s3err/s3err.go b/s3err/s3err.go index a191801..45b2b71 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -124,6 +124,7 @@ const ( ErrRequestNotReadyYet ErrMissingDateHeader ErrGetUploadsWithKey + ErrCopySourceNotAllowed ErrInvalidRequest ErrAuthNotSetup ErrNotImplemented @@ -521,6 +522,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Key is not expected for the GET method ?uploads subresource", HTTPStatusCode: http.StatusBadRequest, }, + ErrCopySourceNotAllowed: { + Code: "InvalidArgument", + Description: "You can only specify a copy source header for copy requests.", + HTTPStatusCode: http.StatusBadRequest, + }, ErrInvalidRequest: { Code: "InvalidRequest", Description: "Invalid Request.", diff --git a/tests/integration/general.go b/tests/integration/general.go index 12ffab6..5bfce5d 100644 --- a/tests/integration/general.go +++ b/tests/integration/general.go @@ -15,6 +15,7 @@ package integration import ( + "fmt" "net/http" "time" @@ -141,6 +142,53 @@ func RouterGetUploadsWithKey(s *S3Conf) error { }) } +func RouterCopySourceNotAllowed(s *S3Conf) error { + testName := "RouterCopySourceNotAllowed" + return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error { + for _, method := range []string{ + http.MethodPost, + http.MethodDelete, + http.MethodGet, + http.MethodHead, + } { + for _, path := range []string{ + "/bucket", + "/bucket/object", + } { + if method == http.MethodPost { + // the error for POST request occurs only when uploadId is there + path += "?uploadId=something" + } + + req, err := http.NewRequest(method, s.endpoint+path, nil) + if err != nil { + return fmt.Errorf("failed to make %s request to %s", method, path) + } + + req.Header.Add("x-amz-copy-source", "bucket/object") + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send %s request to %s", method, path) + } + + if method == http.MethodHead { + // for head requests only check the status code + if resp.StatusCode != http.StatusBadRequest { + return fmt.Errorf("expected 400 status code for HEAD %s request, instead got %v", path, resp.StatusCode) + } + } else { + if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)); err != nil { + return fmt.Errorf("%s %s: %w", method, path, err) + } + } + } + } + + return nil + }) +} + // CORS middleware tests func CORSMiddleware_invalid_method(s *S3Conf) error { testName := "CORSMiddleware_invalid_method" diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index c2fedea..fc30684 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -1087,6 +1087,7 @@ func TestRouter(ts *TestState) { ts.Run(RouterPostObjectWithoutQuery) ts.Run(RouterPUTObjectOnlyUploadId) ts.Run(RouterGetUploadsWithKey) + ts.Run(RouterCopySourceNotAllowed) } type IntTest func(s3 *S3Conf) error @@ -1724,5 +1725,6 @@ func GetIntTests() IntTests { "RouterPostObjectWithoutQuery": RouterPostObjectWithoutQuery, "RouterPUTObjectOnlyUploadId": RouterPUTObjectOnlyUploadId, "RouterGetUploadsWithKey": RouterGetUploadsWithKey, + "RouterCopySourceNotAllowed": RouterCopySourceNotAllowed, } }