From a3fef4254ae43915952520cdf97d9f1eff61368d Mon Sep 17 00:00:00 2001 From: niksis02 Date: Wed, 18 Jun 2025 23:11:24 +0400 Subject: [PATCH] feat: implements advanced routing for object DELETE and POST actions. fixes #896 fixes #899 Registeres an all route matcher handler at the end of the router to handle the cases when the api call doesn't match to any s3 action. The all routes matcher returns `MethodNotAllowed` for this kind of requests. --- s3api/controllers/base.go | 379 ----------------------------- s3api/controllers/base_test.go | 264 -------------------- s3api/controllers/object-delete.go | 207 ++++++++++++++++ s3api/controllers/object-post.go | 376 ++++++++++++++++++++++++++++ s3api/controllers/utilities.go | 9 + s3api/router.go | 107 ++++---- 6 files changed, 648 insertions(+), 694 deletions(-) create mode 100644 s3api/controllers/object-delete.go create mode 100644 s3api/controllers/object-post.go diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index fae7a415..f3487983 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -1938,385 +1938,6 @@ const ( timefmt = "Mon, 02 Jan 2006 15:04:05 GMT" ) -func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { - bucket := ctx.Params("bucket") - key := ctx.Params("key") - keyEnd := ctx.Params("*1") - uploadId := ctx.Query("uploadId") - acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) - isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) - IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) - parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) - contentType := ctx.Get("Content-Type") - contentDisposition := ctx.Get("Content-Disposition") - contentLanguage := ctx.Get("Content-Language") - cacheControl := ctx.Get("Cache-Control") - contentEncoding := ctx.Get("Content-Encoding") - tagging := ctx.Get("X-Amz-Tagging") - - if keyEnd != "" { - key = strings.Join([]string{key, keyEnd}, "/") - } - - path := ctx.Path() - if path[len(path)-1:] == "/" && key[len(key)-1:] != "/" { - key = key + "/" - } - - if ctx.Request().URI().QueryArgs().Has("restore") { - var restoreRequest types.RestoreRequest - if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil { - if !errors.Is(err, io.EOF) { - if c.debug { - debuglogger.Logf("failed to parse the request body: %v", err) - } - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionRestoreObject, - BucketOwner: parsedAcl.Owner, - }) - } - } - err := auth.VerifyAccess(ctx.Context(), c.be, - auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.RestoreObjectAction, - IsBucketPublic: IsBucketPublic, - }) - if err != nil { - return SendResponse(ctx, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionRestoreObject, - BucketOwner: parsedAcl.Owner, - }) - } - - err = c.be.RestoreObject(ctx.Context(), &s3.RestoreObjectInput{ - Bucket: &bucket, - Key: &key, - RestoreRequest: &restoreRequest, - }) - return SendResponse(ctx, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - EvSender: c.evSender, - Action: metrics.ActionRestoreObject, - BucketOwner: parsedAcl.Owner, - EventName: s3event.EventObjectRestoreCompleted, - }) - } - - if ctx.Request().URI().QueryArgs().Has("select") && ctx.Query("select-type") == "2" { - var payload s3response.SelectObjectContentPayload - - err := xml.Unmarshal(ctx.Body(), &payload) - if err != nil { - if c.debug { - debuglogger.Logf("error unmarshalling select object content: %v", err) - } - return SendXMLResponse(ctx, nil, - s3err.GetAPIError(s3err.ErrMalformedXML), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionSelectObjectContent, - BucketOwner: parsedAcl.Owner, - }) - } - - err = auth.VerifyAccess(ctx.Context(), c.be, - auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAction, - IsBucketPublic: IsBucketPublic, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionSelectObjectContent, - BucketOwner: parsedAcl.Owner, - }) - } - - sw := c.be.SelectObjectContent(ctx.Context(), - &s3.SelectObjectContentInput{ - Bucket: &bucket, - Key: &key, - Expression: payload.Expression, - ExpressionType: payload.ExpressionType, - InputSerialization: payload.InputSerialization, - OutputSerialization: payload.OutputSerialization, - RequestProgress: payload.RequestProgress, - ScanRange: payload.ScanRange, - }) - - ctx.Context().SetBodyStreamWriter(sw) - - return nil - } - - if uploadId != "" { - data := struct { - Parts []types.CompletedPart `xml:"Part"` - }{} - - err := xml.Unmarshal(ctx.Body(), &data) - if err != nil { - if c.debug { - debuglogger.Logf("error unmarshalling complete multipart upload: %v", err) - } - return SendXMLResponse(ctx, nil, - s3err.GetAPIError(s3err.ErrMalformedXML), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - if len(data.Parts) == 0 { - if c.debug { - debuglogger.Logf("empty parts provided for complete multipart upload") - } - return SendXMLResponse(ctx, nil, - s3err.GetAPIError(s3err.ErrEmptyParts), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - var mpuObjectSize *int64 - mpuObjSizeHdr := ctx.Get("X-Amz-Mp-Object-Size") - if mpuObjSizeHdr != "" { - val, err := strconv.ParseInt(mpuObjSizeHdr, 10, 64) - //TODO: Not sure if invalid request should be returned - if err != nil { - if c.debug { - debuglogger.Logf("invalid value for 'x-amz-mp-objects-size' header: %v", err) - } - return SendXMLResponse(ctx, nil, - s3err.GetAPIError(s3err.ErrInvalidRequest), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - if val < 0 { - debuglogger.Logf("value for 'x-amz-mp-objects-size' header is less than 0: %v", val) - return SendXMLResponse(ctx, nil, - s3err.GetInvalidMpObjectSizeErr(val), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - mpuObjectSize = &val - } - - err = auth.VerifyAccess(ctx.Context(), c.be, - auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectAction, - IsBucketPublic: IsBucketPublic, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - _, checksums, err := utils.ParseChecksumHeaders(ctx) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - checksumType := types.ChecksumType(ctx.Get("x-amz-checksum-type")) - err = utils.IsChecksumTypeValid(checksumType) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - res, versid, err := c.be.CompleteMultipartUpload(ctx.Context(), - &s3.CompleteMultipartUploadInput{ - Bucket: &bucket, - Key: &key, - UploadId: &uploadId, - MultipartUpload: &types.CompletedMultipartUpload{ - Parts: data.Parts, - }, - MpuObjectSize: mpuObjectSize, - ChecksumCRC32: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32]), - ChecksumCRC32C: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32c]), - ChecksumSHA1: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha1]), - ChecksumSHA256: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha256]), - ChecksumCRC64NVME: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc64nvme]), - ChecksumType: checksumType, - }) - if err == nil { - if versid != "" { - utils.SetResponseHeaders(ctx, []utils.CustomHeader{ - { - Key: "x-amz-version-id", - Value: versid, - }, - }) - } - return SendXMLResponse(ctx, res, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - EvSender: c.evSender, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - ObjectETag: res.ETag, - EventName: s3event.EventCompleteMultipartUpload, - VersionId: backend.GetPtrFromString(versid), - }) - } - return SendXMLResponse(ctx, res, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCompleteMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - err := auth.VerifyAccess(ctx.Context(), c.be, - auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectAction, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCreateMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - objLockState, err := utils.ParsObjectLockHdrs(ctx) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCreateMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - metadata := utils.GetUserMetaData(&ctx.Request().Header) - - checksumAlgorithm, checksumType, err := utils.ParseCreateMpChecksumHeaders(ctx) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCreateMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) - } - - expires := ctx.Get("Expires") - - res, err := c.be.CreateMultipartUpload(ctx.Context(), - s3response.CreateMultipartUploadInput{ - Bucket: &bucket, - Key: &key, - Tagging: &tagging, - ContentType: &contentType, - ContentEncoding: &contentEncoding, - ContentDisposition: &contentDisposition, - ContentLanguage: &contentLanguage, - CacheControl: &cacheControl, - Expires: &expires, - ObjectLockRetainUntilDate: &objLockState.RetainUntilDate, - ObjectLockMode: objLockState.ObjectLockMode, - ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus, - Metadata: metadata, - ChecksumAlgorithm: checksumAlgorithm, - ChecksumType: checksumType, - }) - if err == nil { - if checksumAlgorithm != "" { - utils.SetResponseHeaders(ctx, []utils.CustomHeader{ - { - Key: "x-amz-checksum-algorithm", - Value: string(checksumAlgorithm), - }, - }) - } - } - return SendXMLResponse(ctx, res, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionCreateMultipartUpload, - BucketOwner: parsedAcl.Owner, - }) -} - type MetaOpts struct { Logger s3log.AuditLogger EvSender s3event.S3EventSender diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index 652e07e6..65eb7e4e 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -15,7 +15,6 @@ package controllers import ( - "bufio" "context" "encoding/json" "fmt" @@ -837,269 +836,6 @@ func TestS3ApiController_DeleteObjects(t *testing.T) { } } -func TestS3ApiController_DeleteActions(t *testing.T) { - type args struct { - req *http.Request - } - - app := fiber.New() - s3ApiController := S3ApiController{ - be: &BackendMock{ - GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { - return acldata, nil - }, - DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) { - return &s3.DeleteObjectOutput{}, nil - }, - AbortMultipartUploadFunc: func(context.Context, *s3.AbortMultipartUploadInput) error { - return nil - }, - DeleteObjectTaggingFunc: func(_ context.Context, bucket, object string) error { - return nil - }, - GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { - return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) - }, - }, - } - - app.Use(func(ctx *fiber.Ctx) error { - utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) - utils.ContextKeyIsRoot.Set(ctx, true) - utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) - return ctx.Next() - }) - app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions) - - // Error case - appErr := fiber.New() - - s3ApiControllerErr := S3ApiController{be: &BackendMock{ - GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { - return acldata, nil - }, - DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) - }, - GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { - return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) - }, - }} - - appErr.Use(func(ctx *fiber.Ctx) error { - utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) - utils.ContextKeyIsRoot.Set(ctx, true) - utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) - return ctx.Next() - }) - appErr.Delete("/:bucket/:key/*", s3ApiControllerErr.DeleteActions) - - tests := []struct { - name string - app *fiber.App - args args - wantErr bool - statusCode int - }{ - { - name: "Abort-multipart-upload-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key?uploadId=324234", nil), - }, - wantErr: false, - statusCode: 204, - }, - { - name: "Remove-object-tagging-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key/key2?tagging", nil), - }, - wantErr: false, - statusCode: 204, - }, - { - name: "Delete-object-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key", nil), - }, - wantErr: false, - statusCode: 204, - }, - { - name: "Delete-object-error", - app: appErr, - args: args{ - req: httptest.NewRequest(http.MethodDelete, "/my-bucket/invalid-key", nil), - }, - wantErr: false, - statusCode: 404, - }, - } - for _, tt := range tests { - resp, err := tt.app.Test(tt.args.req) - - if (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.DeleteActions() error = %v, wantErr %v", err, tt.wantErr) - } - - if resp.StatusCode != tt.statusCode { - t.Errorf("S3ApiController.DeleteActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) - } - } -} - -func TestS3ApiController_CreateActions(t *testing.T) { - type args struct { - req *http.Request - } - app := fiber.New() - s3ApiController := S3ApiController{ - be: &BackendMock{ - GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { - return acldata, nil - }, - RestoreObjectFunc: func(context.Context, *s3.RestoreObjectInput) error { - return nil - }, - CompleteMultipartUploadFunc: func(context.Context, *s3.CompleteMultipartUploadInput) (s3response.CompleteMultipartUploadResult, string, error) { - return s3response.CompleteMultipartUploadResult{}, "", nil - }, - CreateMultipartUploadFunc: func(context.Context, s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) { - return s3response.InitiateMultipartUploadResult{}, nil - }, - SelectObjectContentFunc: func(context.Context, *s3.SelectObjectContentInput) func(w *bufio.Writer) { - return func(w *bufio.Writer) {} - }, - }, - } - - bdy := ` - - string - string - - ` - - completMpBody := ` - - - etag - 1 - - - ` - - completMpEmptyBody := ` - - ` - - app.Use(func(ctx *fiber.Ctx) error { - utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) - utils.ContextKeyIsRoot.Set(ctx, true) - utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) - return ctx.Next() - }) - app.Post("/:bucket/:key/*", s3ApiController.CreateActions) - - invChecksumAlgo := httptest.NewRequest(http.MethodPost, "/my-bucket/my-key", nil) - invChecksumAlgo.Header.Set("X-Amz-Checksum-Algorithm", "invalid_checksum_algorithm") - - tests := []struct { - name string - app *fiber.App - args args - wantErr bool - statusCode int - }{ - { - name: "Restore-object-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?restore", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "Select-object-content-invalid-body", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", nil), - }, - wantErr: false, - statusCode: 400, - }, - { - name: "Select-object-content-invalid-body", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", strings.NewReader(bdy)), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "Complete-multipart-upload-error", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", nil), - }, - wantErr: false, - statusCode: 400, - }, - { - name: "Complete-multipart-upload-empty-parts", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", strings.NewReader(completMpEmptyBody)), - }, - wantErr: false, - statusCode: 400, - }, - { - name: "Complete-multipart-upload-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", strings.NewReader(completMpBody)), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "Create-multipart-upload-invalid-checksum-algorithm", - app: app, - args: args{ - req: invChecksumAlgo, - }, - wantErr: false, - statusCode: 400, - }, - { - name: "Create-multipart-upload-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key", nil), - }, - wantErr: false, - statusCode: 200, - }, - } - for _, tt := range tests { - resp, err := tt.app.Test(tt.args.req) - - if (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.CreateActions() error = %v, wantErr %v", err, tt.wantErr) - } - - if resp.StatusCode != tt.statusCode { - t.Errorf("S3ApiController.CreateActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) - } - } -} - func Test_XMLresponse(t *testing.T) { type args struct { ctx *fiber.Ctx diff --git a/s3api/controllers/object-delete.go b/s3api/controllers/object-delete.go new file mode 100644 index 00000000..2cbebae2 --- /dev/null +++ b/s3api/controllers/object-delete.go @@ -0,0 +1,207 @@ +// 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 controllers + +import ( + "fmt" + "net/http" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/metrics" + "github.com/versity/versitygw/s3api/utils" + "github.com/versity/versitygw/s3event" +) + +func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + + err := auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.DeleteObjectTaggingAction, + IsBucketPublic: IsBucketPublic, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionDeleteObjectTagging, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + err = c.be.DeleteObjectTagging(ctx.Context(), bucket, key) + return &Response{ + MetaOpts: &MetaOptions{ + Status: http.StatusNoContent, + Action: metrics.ActionDeleteObjectTagging, + BucketOwner: parsedAcl.Owner, + EventName: s3event.EventObjectTaggingDelete, + }, + }, err +} + +func (c S3ApiController) AbortMultipartUplaod(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + uploadId := ctx.Query("uploadId") + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + + err := auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.AbortMultipartUploadAction, + IsBucketPublic: IsBucketPublic, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionAbortMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + err = c.be.AbortMultipartUpload(ctx.Context(), + &s3.AbortMultipartUploadInput{ + UploadId: &uploadId, + Bucket: &bucket, + Key: &key, + }) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionAbortMultipartUpload, + BucketOwner: parsedAcl.Owner, + Status: http.StatusNoContent, + }, + }, err +} + +func (c S3ApiController) DeleteObject(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + versionId := ctx.Query("versionId") + bypass := strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true") + // context locals + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + + //TODO: check s3:DeleteObjectVersion policy in case a use tries to delete a version of an object + + err := auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.DeleteObjectAction, + IsBucketPublic: IsBucketPublic, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionDeleteObject, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + err = auth.CheckObjectAccess( + ctx.Context(), + bucket, + acct.Access, + []types.ObjectIdentifier{ + { + Key: &key, + VersionId: &versionId, + }, + }, + bypass, + IsBucketPublic, + c.be, + ) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionDeleteObject, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + res, err := c.be.DeleteObject(ctx.Context(), + &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &key, + VersionId: &versionId, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionDeleteObject, + BucketOwner: parsedAcl.Owner, + EventName: s3event.EventObjectRemovedDelete, + Status: http.StatusNoContent, + }, + }, err + } + + headers := map[string]*string{ + "x-amz-version-id": res.VersionId, + } + + if res.DeleteMarker != nil && *res.DeleteMarker { + headers["x-amz-delete-marker"] = utils.GetStringPtr("true") + } + + return &Response{ + Headers: headers, + MetaOpts: &MetaOptions{ + Action: metrics.ActionDeleteObject, + BucketOwner: parsedAcl.Owner, + EventName: s3event.EventObjectRemovedDelete, + Status: http.StatusNoContent, + }, + }, nil +} diff --git a/s3api/controllers/object-post.go b/s3api/controllers/object-post.go new file mode 100644 index 00000000..bbe96643 --- /dev/null +++ b/s3api/controllers/object-post.go @@ -0,0 +1,376 @@ +// 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 controllers + +import ( + "encoding/xml" + "fmt" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/metrics" + "github.com/versity/versitygw/s3api/debuglogger" + "github.com/versity/versitygw/s3api/utils" + "github.com/versity/versitygw/s3err" + "github.com/versity/versitygw/s3event" + "github.com/versity/versitygw/s3response" +) + +func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + + var restoreRequest types.RestoreRequest + if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil { + debuglogger.Logf("failed to parse the request body: %v", err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionRestoreObject, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrMalformedXML) + } + err := auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.RestoreObjectAction, + IsBucketPublic: IsBucketPublic, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionRestoreObject, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + err = c.be.RestoreObject(ctx.Context(), &s3.RestoreObjectInput{ + Bucket: &bucket, + Key: &key, + RestoreRequest: &restoreRequest, + }) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionRestoreObject, + BucketOwner: parsedAcl.Owner, + EventName: s3event.EventObjectRestoreCompleted, + }, + }, err +} + +func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + + var payload s3response.SelectObjectContentPayload + + err := xml.Unmarshal(ctx.Body(), &payload) + if err != nil { + debuglogger.Logf("error unmarshalling select object content: %v", err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionSelectObjectContent, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrMalformedXML) + } + + err = auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAction, + IsBucketPublic: IsBucketPublic, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionSelectObjectContent, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + sw := c.be.SelectObjectContent(ctx.Context(), + &s3.SelectObjectContentInput{ + Bucket: &bucket, + Key: &key, + Expression: payload.Expression, + ExpressionType: payload.ExpressionType, + InputSerialization: payload.InputSerialization, + OutputSerialization: payload.OutputSerialization, + RequestProgress: payload.RequestProgress, + ScanRange: payload.ScanRange, + }) + + ctx.Context().SetBodyStreamWriter(sw) + + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionSelectObjectContent, + BucketOwner: parsedAcl.Owner, + }, + }, nil +} + +func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + contentType := ctx.Get("Content-Type") + contentDisposition := ctx.Get("Content-Disposition") + contentLanguage := ctx.Get("Content-Language") + cacheControl := ctx.Get("Cache-Control") + contentEncoding := ctx.Get("Content-Encoding") + tagging := ctx.Get("X-Amz-Tagging") + expires := ctx.Get("Expires") + metadata := utils.GetUserMetaData(&ctx.Request().Header) + // context locals + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + + err := auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectAction, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCreateMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + objLockState, err := utils.ParsObjectLockHdrs(ctx) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCreateMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + checksumAlgorithm, checksumType, err := utils.ParseCreateMpChecksumHeaders(ctx) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCreateMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + res, err := c.be.CreateMultipartUpload(ctx.Context(), + s3response.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &key, + Tagging: &tagging, + ContentType: &contentType, + ContentEncoding: &contentEncoding, + ContentDisposition: &contentDisposition, + ContentLanguage: &contentLanguage, + CacheControl: &cacheControl, + Expires: &expires, + ObjectLockRetainUntilDate: &objLockState.RetainUntilDate, + ObjectLockMode: objLockState.ObjectLockMode, + ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus, + Metadata: metadata, + ChecksumAlgorithm: checksumAlgorithm, + ChecksumType: checksumType, + }) + var headers map[string]*string + if err == nil { + headers = map[string]*string{ + "x-amz-checksum-algorithm": utils.ConvertToStringPtr(checksumAlgorithm), + } + } + return &Response{ + Headers: headers, + Data: res, + MetaOpts: &MetaOptions{ + Action: metrics.ActionCreateMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + uploadId := ctx.Query("uploadId") + mpuObjSizeHdr := ctx.Get("X-Amz-Mp-Object-Size") + checksumType := types.ChecksumType(ctx.Get("x-amz-checksum-type")) + // context locals + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + + err := auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectAction, + IsBucketPublic: IsBucketPublic, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + data := struct { + Parts []types.CompletedPart `xml:"Part"` + }{} + + err = xml.Unmarshal(ctx.Body(), &data) + if err != nil { + debuglogger.Logf("error unmarshalling complete multipart upload: %v", err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrMalformedXML) + } + + if len(data.Parts) == 0 { + debuglogger.Logf("empty parts provided for complete multipart upload") + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrEmptyParts) + } + + var mpuObjectSize *int64 + if mpuObjSizeHdr != "" { + val, err := strconv.ParseInt(mpuObjSizeHdr, 10, 64) + //TODO: Not sure if invalid request should be returned + if err != nil { + debuglogger.Logf("invalid value for 'x-amz-mp-objects-size' header: %v", err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + if val < 0 { + debuglogger.Logf("value for 'x-amz-mp-objects-size' header is less than 0: %v", val) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetInvalidMpObjectSizeErr(val) + } + + mpuObjectSize = &val + } + + _, checksums, err := utils.ParseChecksumHeaders(ctx) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + err = utils.IsChecksumTypeValid(checksumType) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + res, versid, err := c.be.CompleteMultipartUpload(ctx.Context(), + &s3.CompleteMultipartUploadInput{ + Bucket: &bucket, + Key: &key, + UploadId: &uploadId, + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: data.Parts, + }, + MpuObjectSize: mpuObjectSize, + ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]), + ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]), + ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]), + ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]), + ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]), + ChecksumType: checksumType, + }) + return &Response{ + Data: res, + Headers: map[string]*string{ + "x-amz-version-id": &versid, + }, + MetaOpts: &MetaOptions{ + Action: metrics.ActionCompleteMultipartUpload, + BucketOwner: parsedAcl.Owner, + ObjectETag: res.ETag, + EventName: s3event.EventCompleteMultipartUpload, + VersionId: &versid, + }, + }, err +} diff --git a/s3api/controllers/utilities.go b/s3api/controllers/utilities.go index f7b579ec..097b747b 100644 --- a/s3api/controllers/utilities.go +++ b/s3api/controllers/utilities.go @@ -183,3 +183,12 @@ func SetResponseHeaders(ctx *fiber.Ctx, headers map[string]*string) { ctx.Response().Header.Add(key, *val) } } + +// Returns MethodNotAllowed for unmatched routes +func (c S3ApiController) HandleUnmatch(ctx *fiber.Ctx) (*Response, error) { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionUndetected, + }, + }, s3err.GetAPIError(s3err.ErrMethodNotAllowed) +} diff --git a/s3api/router.go b/s3api/router.go index bc3187e9..8014e951 100644 --- a/s3api/router.go +++ b/s3api/router.go @@ -54,67 +54,69 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ app.Patch("/list-buckets", middlewares.IsAdmin(logger), adminController.ListBuckets) } - // ListBuckets + // ListBuckets action app.Get("/", controllers.ProcessResponse(ctrl.ListBuckets, logger, evs, mm)) + bucketRouter := app.Group("/:bucket") + objectRouter := app.Group("/:bucket/*") + // PUT bucket operations - app.Put("/:bucket", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.PutBucketTagging, logger, evs, mm)) - app.Put("/:bucket", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.PutBucketOwnershipControls, logger, evs, mm)) - app.Put("/:bucket", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.PutBucketVersioning, logger, evs, mm)) - app.Put("/:bucket", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.PutObjectLockConfiguration, logger, evs, mm)) - app.Put("/:bucket", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.PutBucketCors, logger, evs, mm)) - app.Put("/:bucket", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.PutBucketPolicy, logger, evs, mm)) - app.Put("/:bucket", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.PutBucketAcl, logger, evs, mm)) - app.Put("/:bucket", controllers.ProcessResponse(ctrl.CreateBucket, logger, evs, mm)) + bucketRouter.Put("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.PutBucketTagging, logger, evs, mm)) + bucketRouter.Put("", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.PutBucketOwnershipControls, logger, evs, mm)) + bucketRouter.Put("", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.PutBucketVersioning, logger, evs, mm)) + bucketRouter.Put("", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.PutObjectLockConfiguration, logger, evs, mm)) + bucketRouter.Put("", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.PutBucketCors, logger, evs, mm)) + bucketRouter.Put("", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.PutBucketPolicy, logger, evs, mm)) + bucketRouter.Put("", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.PutBucketAcl, logger, evs, mm)) + bucketRouter.Put("", controllers.ProcessResponse(ctrl.CreateBucket, logger, evs, mm)) - // HeadBucket - app.Head("/:bucket", controllers.ProcessResponse(ctrl.HeadBucket, logger, evs, mm)) + // HeadBucket action + bucketRouter.Head("", controllers.ProcessResponse(ctrl.HeadBucket, logger, evs, mm)) - // Delete bucket operations - app.Delete("/:bucket", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.DeleteBucketTagging, logger, evs, mm)) - app.Delete("/:bucket", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.DeleteBucketOwnershipControls, logger, evs, mm)) - app.Delete("/:bucket", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.DeleteBucketPolicy, logger, evs, mm)) - app.Delete("/:bucket", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.DeleteBucketCors, logger, evs, mm)) - app.Delete("/:bucket", controllers.ProcessResponse(ctrl.DeleteBucket, logger, evs, mm)) + // DELETE bucket operations + bucketRouter.Delete("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.DeleteBucketTagging, logger, evs, mm)) + bucketRouter.Delete("", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.DeleteBucketOwnershipControls, logger, evs, mm)) + bucketRouter.Delete("", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.DeleteBucketPolicy, logger, evs, mm)) + bucketRouter.Delete("", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.DeleteBucketCors, logger, evs, mm)) + bucketRouter.Delete("", controllers.ProcessResponse(ctrl.DeleteBucket, logger, evs, mm)) // GET bucket operations - app.Get("/:bucket", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetBucketTagging, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.GetBucketOwnershipControls, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.GetBucketVersioning, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.GetBucketPolicy, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.GetBucketCors, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.GetObjectLockConfiguration, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetBucketAcl, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("uploads"), controllers.ProcessResponse(ctrl.ListMultipartUploads, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgs("versions"), controllers.ProcessResponse(ctrl.ListObjectVersions, logger, evs, mm)) - app.Get("/:bucket", middlewares.MatchQueryArgWithValue("list-type", "2"), controllers.ProcessResponse(ctrl.ListObjectsV2, logger, evs, mm)) - app.Get("/:bucket", controllers.ProcessResponse(ctrl.ListObjects, logger, evs, mm)) - - // HeadObject - app.Head("/:bucket/*", controllers.ProcessResponse(ctrl.HeadObject, logger, evs, mm)) - - // GET object operations - app.Get("/:bucket/*", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetObjectTagging, logger, evs, mm)) - app.Get("/:bucket/*", middlewares.MatchQueryArgs("retention"), controllers.ProcessResponse(ctrl.GetObjectRetention, logger, evs, mm)) - app.Get("/:bucket/*", middlewares.MatchQueryArgs("legal-hold"), controllers.ProcessResponse(ctrl.GetObjectLegalHold, logger, evs, mm)) - app.Get("/:bucket/*", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetObjectAcl, logger, evs, mm)) - app.Get("/:bucket/*", middlewares.MatchQueryArgs("attributes"), controllers.ProcessResponse(ctrl.GetObjectAttributes, logger, evs, mm)) - app.Get("/:bucket/*", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.ListParts, logger, evs, mm)) - app.Get("/:bucket/*", controllers.ProcessResponse(ctrl.GetObject, logger, evs, mm)) - - // DeleteObject action - // AbortMultipartUpload action - // DeleteObjectTagging action - app.Delete("/:bucket/:key/*", ctrl.DeleteActions) + bucketRouter.Get("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetBucketTagging, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.GetBucketOwnershipControls, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.GetBucketVersioning, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.GetBucketPolicy, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.GetBucketCors, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.GetObjectLockConfiguration, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetBucketAcl, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("uploads"), controllers.ProcessResponse(ctrl.ListMultipartUploads, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgs("versions"), controllers.ProcessResponse(ctrl.ListObjectVersions, logger, evs, mm)) + bucketRouter.Get("", middlewares.MatchQueryArgWithValue("list-type", "2"), controllers.ProcessResponse(ctrl.ListObjectsV2, logger, evs, mm)) + bucketRouter.Get("", controllers.ProcessResponse(ctrl.ListObjects, logger, evs, mm)) // DeleteObjects action - app.Post("/:bucket", ctrl.DeleteObjects) + bucketRouter.Post("", ctrl.DeleteObjects) - // CompleteMultipartUpload action - // CreateMultipartUpload - // RestoreObject action - // SelectObjectContent action - app.Post("/:bucket/:key/*", ctrl.CreateActions) + // HeadObject + objectRouter.Head("", controllers.ProcessResponse(ctrl.HeadObject, logger, evs, mm)) + + // GET object operations + objectRouter.Get("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetObjectTagging, logger, evs, mm)) + objectRouter.Get("", middlewares.MatchQueryArgs("retention"), controllers.ProcessResponse(ctrl.GetObjectRetention, logger, evs, mm)) + objectRouter.Get("", middlewares.MatchQueryArgs("legal-hold"), controllers.ProcessResponse(ctrl.GetObjectLegalHold, logger, evs, mm)) + objectRouter.Get("", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetObjectAcl, logger, evs, mm)) + objectRouter.Get("", middlewares.MatchQueryArgs("attributes"), controllers.ProcessResponse(ctrl.GetObjectAttributes, logger, evs, mm)) + objectRouter.Get("", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.ListParts, logger, evs, mm)) + objectRouter.Get("", controllers.ProcessResponse(ctrl.GetObject, logger, evs, mm)) + + // DELETE object operations + objectRouter.Delete("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.DeleteObjectTagging, logger, evs, mm)) + objectRouter.Delete("", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.AbortMultipartUplaod, logger, evs, mm)) + objectRouter.Delete("", controllers.ProcessResponse(ctrl.DeleteObject, logger, evs, mm)) + + objectRouter.Post("", middlewares.MatchQueryArgs("restore"), controllers.ProcessResponse(ctrl.RestoreObject, logger, evs, mm)) + objectRouter.Post("", middlewares.MatchQueryArgs("list-type"), middlewares.MatchQueryArgWithValue("list-type", "2"), controllers.ProcessResponse(ctrl.RestoreObject, logger, evs, mm)) + objectRouter.Post("", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.CompleteMultipartUpload, logger, evs, mm)) + objectRouter.Post("", middlewares.MatchQueryArgs("uploads"), controllers.ProcessResponse(ctrl.CreateMultipartUpload, logger, evs, mm)) // CopyObject action // PutObject action @@ -123,4 +125,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ // PutObjectTagging action // PutObjectAcl action app.Put("/:bucket/:key/*", ctrl.PutActions) + + // Return MethodNotAllowed for all the unmatched routes + app.All("*", controllers.ProcessResponse(ctrl.HandleUnmatch, logger, evs, mm)) }