From b8456bc5abbec9377b9be812a03c9e2222218f16 Mon Sep 17 00:00:00 2001 From: niksis02 Date: Thu, 12 Jun 2025 23:31:41 +0400 Subject: [PATCH] feat: implements advanced routing system for the bucket get operations. Closes #908 This PR introduces a new routing system integrated with Fiber. It matches each S3 action to a route using middleware utility functions (e.g., URL query match, request header match). Each S3 action is mapped to a dedicated route in the Fiber router. This functionality cannot be achieved using standard Fiber methods, as Fiber lacks the necessary tooling for such dynamic routing. Additionally, this PR implements a generic response handler to manage responses from the backend. This abstraction helps isolate the controller from the data layer and Fiber-specific response logic. With this approach, controller unit testing becomes simpler and more effective. --- s3api/controllers/base.go | 511 ---------------------- s3api/controllers/base_test.go | 209 --------- s3api/controllers/bucket-get.go | 587 ++++++++++++++++++++++++++ s3api/controllers/utilities.go | 181 ++++++++ s3api/middlewares/router-utilities.go | 49 +++ s3api/router.go | 16 +- s3api/utils/context-keys.go | 5 + 7 files changed, 833 insertions(+), 725 deletions(-) create mode 100644 s3api/controllers/bucket-get.go create mode 100644 s3api/controllers/utilities.go create mode 100644 s3api/middlewares/router-utilities.go diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index e4ecbaed..6f6861c6 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -725,509 +725,6 @@ func getint64(i *int64) int64 { return *i } -func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { - bucket := ctx.Params("bucket") - prefix := ctx.Query("prefix") - cToken := ctx.Query("continuation-token") - sAfter := ctx.Query("start-after") - marker := ctx.Query("marker") - delimiter := ctx.Query("delimiter") - maxkeysStr := ctx.Query("max-keys") - keyMarker := ctx.Query("key-marker") - maxUploadsStr := ctx.Query("max-uploads") - uploadIdMarker := ctx.Query("upload-id-marker") - versionIdMarker := ctx.Query("version-id-marker") - acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) - isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) - isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) - parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) - - if ctx.Request().URI().QueryArgs().Has("tagging") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketTaggingAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketTagging, - BucketOwner: parsedAcl.Owner, - }) - } - - tags, err := c.be.GetBucketTagging(ctx.Context(), bucket) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketTagging, - BucketOwner: parsedAcl.Owner, - }) - } - resp := s3response.Tagging{ - TagSet: s3response.TagSet{Tags: []s3response.Tag{}}, - } - - for key, val := range tags { - resp.TagSet.Tags = append(resp.TagSet.Tags, - s3response.Tag{Key: key, Value: val}) - } - - return SendXMLResponse(ctx, resp, nil, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketTagging, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("ownershipControls") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketOwnershipControlsAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketOwnershipControls, - BucketOwner: parsedAcl.Owner, - }) - } - - data, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket) - return SendXMLResponse(ctx, - s3response.OwnershipControls{ - Rules: []types.OwnershipControlsRule{ - { - ObjectOwnership: data, - }, - }, - }, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketOwnershipControls, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("versioning") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketVersioningAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketVersioning, - BucketOwner: parsedAcl.Owner, - }) - } - // Only admin users and the bucket owner are allowed to get the versioning state of a bucket. - if err := auth.IsAdminOrOwner(acct, isRoot, parsedAcl); err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketVersioning, - BucketOwner: parsedAcl.Owner, - }) - } - - data, err := c.be.GetBucketVersioning(ctx.Context(), bucket) - return SendXMLResponse(ctx, data, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketVersioning, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("policy") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketPolicyAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketPolicy, - BucketOwner: parsedAcl.Owner, - }) - } - - data, err := c.be.GetBucketPolicy(ctx.Context(), bucket) - return SendXMLResponse(ctx, data, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketPolicy, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("cors") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketCorsAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketCors, - BucketOwner: parsedAcl.Owner, - }) - } - - data, err := c.be.GetBucketCors(ctx.Context(), bucket) - return SendXMLResponse(ctx, data, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketCors, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("versions") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketVersionsAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjectVersions, - BucketOwner: parsedAcl.Owner, - }) - } - - maxkeys, err := utils.ParseUint(maxkeysStr) - if err != nil { - if c.debug { - debuglogger.Logf("error parsing max keys %q: %v", - maxkeysStr, err) - } - return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxKeys), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjectVersions, - BucketOwner: parsedAcl.Owner, - }) - } - - data, err := c.be.ListObjectVersions(ctx.Context(), - &s3.ListObjectVersionsInput{ - Bucket: &bucket, - Delimiter: &delimiter, - KeyMarker: &keyMarker, - MaxKeys: &maxkeys, - Prefix: &prefix, - VersionIdMarker: &versionIdMarker, - }) - return SendXMLResponse(ctx, data, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjectVersions, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("object-lock") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketObjectLockConfigurationAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetObjectLockConfiguration, - BucketOwner: parsedAcl.Owner, - }) - } - - data, err := c.be.GetObjectLockConfiguration(ctx.Context(), bucket) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetObjectLockConfiguration, - BucketOwner: parsedAcl.Owner, - }) - } - - resp, err := auth.ParseBucketLockConfigurationOutput(data) - return SendXMLResponse(ctx, resp, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetObjectLockConfiguration, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("acl") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionReadAcp, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketAclAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketAcl, - BucketOwner: parsedAcl.Owner, - }) - } - - data, err := c.be.GetBucketAcl(ctx.Context(), - &s3.GetBucketAclInput{Bucket: &bucket}) - if err != nil { - return SendResponse(ctx, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - }) - } - - res, err := auth.ParseACLOutput(data, parsedAcl.Owner) - return SendXMLResponse(ctx, res, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionGetBucketAcl, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.Request().URI().QueryArgs().Has("uploads") { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketMultipartUploadsAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListMultipartUploads, - BucketOwner: parsedAcl.Owner, - }) - } - maxUploads, err := utils.ParseUint(maxUploadsStr) - if err != nil { - if c.debug { - debuglogger.Logf("error parsing max uploads %q: %v", - maxUploadsStr, err) - } - return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxUploads), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListMultipartUploads, - BucketOwner: parsedAcl.Owner, - }) - } - res, err := c.be.ListMultipartUploads(ctx.Context(), - &s3.ListMultipartUploadsInput{ - Bucket: &bucket, - Delimiter: &delimiter, - Prefix: &prefix, - UploadIdMarker: &uploadIdMarker, - MaxUploads: &maxUploads, - KeyMarker: &keyMarker, - }) - return SendXMLResponse(ctx, res, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListMultipartUploads, - BucketOwner: parsedAcl.Owner, - }) - } - - if ctx.QueryInt("list-type") == 2 { - err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjectsV2, - BucketOwner: parsedAcl.Owner, - }) - } - maxkeys, err := utils.ParseUint(maxkeysStr) - if err != nil { - if c.debug { - debuglogger.Logf("error parsing max keys %q: %v", - maxkeysStr, err) - } - return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxKeys), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjectsV2, - BucketOwner: parsedAcl.Owner, - }) - } - - fetchOwner := strings.EqualFold(ctx.Query("fetch-owner"), "true") - res, err := c.be.ListObjectsV2(ctx.Context(), - &s3.ListObjectsV2Input{ - Bucket: &bucket, - Prefix: &prefix, - ContinuationToken: &cToken, - Delimiter: &delimiter, - MaxKeys: &maxkeys, - StartAfter: &sAfter, - FetchOwner: &fetchOwner, - }) - return SendXMLResponse(ctx, res, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjectsV2, - 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, - Action: auth.ListBucketAction, - IsBucketPublic: isPublicBucket, - }) - if err != nil { - return SendXMLResponse(ctx, nil, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjects, - BucketOwner: parsedAcl.Owner, - }) - } - - maxkeys, err := utils.ParseUint(maxkeysStr) - if err != nil { - if c.debug { - debuglogger.Logf("error parsing max keys %q: %v", - maxkeysStr, err) - } - return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxKeys), - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjects, - BucketOwner: parsedAcl.Owner, - }) - } - - res, err := c.be.ListObjects(ctx.Context(), - &s3.ListObjectsInput{ - Bucket: &bucket, - Prefix: &prefix, - Marker: &marker, - Delimiter: &delimiter, - MaxKeys: &maxkeys, - }) - return SendXMLResponse(ctx, res, err, - &MetaOpts{ - Logger: c.logger, - MetricsMng: c.mm, - Action: metrics.ActionListObjects, - BucketOwner: parsedAcl.Owner, - }) -} - func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { bucket := ctx.Params("bucket") acl := ctx.Get("X-Amz-Acl") @@ -3949,14 +3446,6 @@ func SendResponse(ctx *fiber.Ctx, err error, l *MetaOpts) error { return nil } -var ( - xmlhdr = []byte(`` + "\n") -) - -const ( - maxXMLBodyLen = 4 * 1024 * 1024 -) - func SendXMLResponse(ctx *fiber.Ctx, resp any, err error, l *MetaOpts) error { if l.MetricsMng != nil { if l.ObjectCount > 0 { diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index f9d83d54..25e57bea 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -364,215 +364,6 @@ func TestS3ApiController_GetActions(t *testing.T) { } } -func TestS3ApiController_ListActions(t *testing.T) { - type args struct { - req *http.Request - } - - objectLockResult, err := json.Marshal(auth.BucketLockConfig{}) - if err != nil { - t.Errorf("failed to parse object lock result %v", err) - } - - app := fiber.New() - s3ApiController := S3ApiController{ - be: &BackendMock{ - GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { - return acldata, nil - }, - ListMultipartUploadsFunc: func(_ context.Context, output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) { - return s3response.ListMultipartUploadsResult{}, nil - }, - ListObjectsV2Func: func(context.Context, *s3.ListObjectsV2Input) (s3response.ListObjectsV2Result, error) { - return s3response.ListObjectsV2Result{}, nil - }, - ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (s3response.ListObjectsResult, error) { - return s3response.ListObjectsResult{}, nil - }, - GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) { - return map[string]string{}, nil - }, - GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (s3response.GetBucketVersioningOutput, error) { - return s3response.GetBucketVersioningOutput{}, nil - }, - ListObjectVersionsFunc: func(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (s3response.ListVersionsResult, error) { - return s3response.ListVersionsResult{}, nil - }, - GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { - return []byte{}, nil - }, - GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { - return objectLockResult, nil - }, - GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) { - return types.ObjectOwnershipBucketOwnerEnforced, nil - }, - }, - } - - 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.Get("/:bucket", s3ApiController.ListActions) - - // Error case - s3ApiControllerError := S3ApiController{ - be: &BackendMock{ - GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { - return acldata, nil - }, - ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (s3response.ListObjectsResult, error) { - return s3response.ListObjectsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented) - }, - GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) - }, - }, - } - appError := fiber.New() - appError.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() - }) - appError.Get("/:bucket", s3ApiControllerError.ListActions) - - tests := []struct { - name string - app *fiber.App - args args - wantErr bool - statusCode int - }{ - { - name: "Get-bucket-tagging-non-existing-bucket", - app: appError, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?tagging", nil), - }, - wantErr: false, - statusCode: 404, - }, - { - name: "Get-bucket-ownership-control-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?ownershipControls", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "Get-bucket-tagging-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?tagging", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "Get-object-lock-configuration-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?object-lock", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "Get-bucket-acl-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?acl=acl", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "List-Multipart-Upload-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?uploads=uploads", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "List-Objects-V2-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?list-type=2", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "List-Objects-V1-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "List-actions-error-case", - app: appError, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket", nil), - }, - wantErr: false, - statusCode: 501, - }, - { - name: "List-actions-get-bucket-versioning-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?versioning", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "List-actions-get-bucket-policy-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?policy", nil), - }, - wantErr: false, - statusCode: 200, - }, - { - name: "List-actions-list-object-versions-success", - app: app, - args: args{ - req: httptest.NewRequest(http.MethodGet, "/my-bucket?versions", nil), - }, - wantErr: false, - statusCode: 200, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resp, err := tt.app.Test(tt.args.req) - - if (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.ListActions() error = %v, wantErr %v", err, tt.wantErr) - } - - if resp.StatusCode != tt.statusCode { - t.Errorf("S3ApiController.ListActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) - } - }) - } -} - func TestS3ApiController_PutBucketActions(t *testing.T) { type args struct { req *http.Request diff --git a/s3api/controllers/bucket-get.go b/s3api/controllers/bucket-get.go new file mode 100644 index 00000000..d115519b --- /dev/null +++ b/s3api/controllers/bucket-get.go @@ -0,0 +1,587 @@ +// 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 ( + "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/s3response" +) + +func (c S3ApiController) GetBucketTagging(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketTaggingAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketTagging, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + tags, err := c.be.GetBucketTagging(ctx.Context(), bucket) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketTagging, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + resp := s3response.Tagging{ + TagSet: s3response.TagSet{Tags: []s3response.Tag{}}, + } + + for key, val := range tags { + resp.TagSet.Tags = append(resp.TagSet.Tags, + s3response.Tag{Key: key, Value: val}) + } + + return &Response{ + Data: resp, + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketTagging, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) GetBucketOwnershipControls(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketOwnershipControlsAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketOwnershipControls, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + data, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket) + return &Response{ + Data: s3response.OwnershipControls{ + Rules: []types.OwnershipControlsRule{ + { + ObjectOwnership: data, + }, + }, + }, + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketOwnershipControls, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) GetBucketVersioning(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketVersioningAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketVersioning, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + // Only admin users and the bucket owner are allowed to get the versioning state of a bucket. + if err := auth.IsAdminOrOwner(acct, isRoot, parsedAcl); err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketVersioning, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + data, err := c.be.GetBucketVersioning(ctx.Context(), bucket) + return &Response{ + Data: data, + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketVersioning, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) GetBucketCors(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketCorsAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketCors, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + data, err := c.be.GetBucketCors(ctx.Context(), bucket) + return &Response{ + Data: data, + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketCors, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) GetBucketPolicy(ctx *fiber.Ctx) (*Response, error) { + bucket := ctx.Params("bucket") + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketPolicyAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketPolicy, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + data, err := c.be.GetBucketPolicy(ctx.Context(), bucket) + return &Response{ + Data: data, + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketPolicy, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) ListObjectVersions(ctx *fiber.Ctx) (*Response, error) { + // url values + bucket := ctx.Params("bucket") + prefix := ctx.Query("prefix") + delimiter := ctx.Query("delimiter") + maxkeysStr := ctx.Query("max-keys") + keyMarker := ctx.Query("key-marker") + versionIdMarker := ctx.Query("version-id-marker") + // context keys + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketVersionsAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjectVersions, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + maxkeys, err := utils.ParseUint(maxkeysStr) + if err != nil { + debuglogger.Logf("error parsing max keys %q: %v", + maxkeysStr, err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjectVersions, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrInvalidMaxKeys) + } + + data, err := c.be.ListObjectVersions(ctx.Context(), + &s3.ListObjectVersionsInput{ + Bucket: &bucket, + Delimiter: &delimiter, + KeyMarker: &keyMarker, + MaxKeys: &maxkeys, + Prefix: &prefix, + VersionIdMarker: &versionIdMarker, + }) + return &Response{ + Data: data, + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjectVersions, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) GetObjectLockConfiguration(ctx *fiber.Ctx) (*Response, error) { + // url values + bucket := ctx.Params("bucket") + // context keys + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketObjectLockConfigurationAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetObjectLockConfiguration, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + data, err := c.be.GetObjectLockConfiguration(ctx.Context(), bucket) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetObjectLockConfiguration, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + resp, err := auth.ParseBucketLockConfigurationOutput(data) + return &Response{ + Data: resp, + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetObjectLockConfiguration, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) GetBucketAcl(ctx *fiber.Ctx) (*Response, error) { + // url values + bucket := ctx.Params("bucket") + // context keys + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionReadAcp, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketAclAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketAcl, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + data, err := c.be.GetBucketAcl(ctx.Context(), + &s3.GetBucketAclInput{Bucket: &bucket}) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketAcl, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + res, err := auth.ParseACLOutput(data, parsedAcl.Owner) + return &Response{ + Data: res, + MetaOpts: &MetaOptions{ + Action: metrics.ActionGetBucketAcl, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) ListMultipartUploads(ctx *fiber.Ctx) (*Response, error) { + // url values + bucket := ctx.Params("bucket") + prefix := ctx.Query("prefix") + delimiter := ctx.Query("delimiter") + keyMarker := ctx.Query("key-marker") + maxUploadsStr := ctx.Query("max-uploads") + uploadIdMarker := ctx.Query("upload-id-marker") + // context keys + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketMultipartUploadsAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListMultipartUploads, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + maxUploads, err := utils.ParseUint(maxUploadsStr) + if err != nil { + debuglogger.Logf("error parsing max uploads %q: %v", + maxUploadsStr, err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListMultipartUploads, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrInvalidMaxUploads) + } + res, err := c.be.ListMultipartUploads(ctx.Context(), + &s3.ListMultipartUploadsInput{ + Bucket: &bucket, + Delimiter: &delimiter, + Prefix: &prefix, + UploadIdMarker: &uploadIdMarker, + MaxUploads: &maxUploads, + KeyMarker: &keyMarker, + }) + return &Response{ + Data: res, + MetaOpts: &MetaOptions{ + Action: metrics.ActionListMultipartUploads, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) ListObjectsV2(ctx *fiber.Ctx) (*Response, error) { + // url values + bucket := ctx.Params("bucket") + prefix := ctx.Query("prefix") + cToken := ctx.Query("continuation-token") + sAfter := ctx.Query("start-after") + delimiter := ctx.Query("delimiter") + maxkeysStr := ctx.Query("max-keys") + fetchOwner := strings.EqualFold(ctx.Query("fetch-owner"), "true") + // context locals + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjectsV2, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + maxkeys, err := utils.ParseUint(maxkeysStr) + if err != nil { + debuglogger.Logf("error parsing max keys %q: %v", + maxkeysStr, err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjectsV2, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrInvalidMaxKeys) + } + + res, err := c.be.ListObjectsV2(ctx.Context(), + &s3.ListObjectsV2Input{ + Bucket: &bucket, + Prefix: &prefix, + ContinuationToken: &cToken, + Delimiter: &delimiter, + MaxKeys: &maxkeys, + StartAfter: &sAfter, + FetchOwner: &fetchOwner, + }) + return &Response{ + Data: res, + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjectsV2, + BucketOwner: parsedAcl.Owner, + }, + }, err +} + +func (c S3ApiController) ListObjects(ctx *fiber.Ctx) (*Response, error) { + // url values + bucket := ctx.Params("bucket") + prefix := ctx.Query("prefix") + marker := ctx.Query("marker") + delimiter := ctx.Query("delimiter") + maxkeysStr := ctx.Query("max-keys") + // context locals + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := 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.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsBucketPublic: isPublicBucket, + }) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjects, + BucketOwner: parsedAcl.Owner, + }, + }, err + } + + maxkeys, err := utils.ParseUint(maxkeysStr) + if err != nil { + debuglogger.Logf("error parsing max keys %q: %v", + maxkeysStr, err) + return &Response{ + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjects, + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrInvalidMaxKeys) + } + + res, err := c.be.ListObjects(ctx.Context(), + &s3.ListObjectsInput{ + Bucket: &bucket, + Prefix: &prefix, + Marker: &marker, + Delimiter: &delimiter, + MaxKeys: &maxkeys, + }) + return &Response{ + Data: res, + MetaOpts: &MetaOptions{ + Action: metrics.ActionListObjects, + BucketOwner: parsedAcl.Owner, + }, + }, err +} diff --git a/s3api/controllers/utilities.go b/s3api/controllers/utilities.go new file mode 100644 index 00000000..97d66a70 --- /dev/null +++ b/s3api/controllers/utilities.go @@ -0,0 +1,181 @@ +// 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" + "net/http" + + "github.com/gofiber/fiber/v2" + "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/s3log" +) + +var ( + xmlhdr = []byte(`` + "\n") +) + +const ( + maxXMLBodyLen = 4 * 1024 * 1024 +) + +type MetaOptions struct { + ContentLength int64 + Action string + BucketOwner string + ObjectSize int64 + ObjectCount int64 + EventName s3event.EventType + ObjectETag *string + VersionId *string + Status int +} + +type Response struct { + Data any + Headers map[string]string + MetaOpts *MetaOptions +} + +type Handler func(ctx *fiber.Ctx) (*Response, error) + +func ProcessResponse(handler Handler, s3logger s3log.AuditLogger, s3evnt s3event.S3EventSender, mm *metrics.Manager) fiber.Handler { + return func(ctx *fiber.Ctx) error { + // if skip locals is set, skip to the next rout handler + if utils.ContextKeySkip.IsSet(ctx) { + utils.ContextKeySkip.Delete(ctx) + return ctx.Next() + } + + response, err := handler(ctx) + opts := response.MetaOpts + // Send the metrics + if mm != nil { + if opts.ObjectCount > 0 { + mm.Send(ctx, err, opts.Action, opts.ObjectCount, opts.Status) + } else { + mm.Send(ctx, err, opts.Action, opts.ContentLength, opts.Status) + } + } + // Handle the error case + if err != nil { + // Audit the error log + if s3logger != nil { + s3logger.Log(ctx, err, nil, s3log.LogMeta{ + Action: opts.Action, + BucketOwner: opts.BucketOwner, + ObjectSize: opts.ObjectSize, + }) + } + serr, ok := err.(s3err.APIError) + if ok { + ctx.Status(serr.HTTPStatusCode) + return ctx.Send(s3err.GetAPIErrorResponse(serr, "", "", "")) + } + + debuglogger.Logf("Internal Error, %v", err) + ctx.Status(http.StatusInternalServerError) + + // If the error is not 's3err.APIError' return 'InternalError' + return ctx.Send(s3err.GetAPIErrorResponse( + s3err.GetAPIError(s3err.ErrInternalError), "", "", "")) + } + + // Set the response headers + SetResponseHeaders(ctx, response.Headers) + + if opts.Status == 0 { + opts.Status = http.StatusOK + } + + // if no data payload is provided, send the response status + if response.Data == nil { + ctx.Status(opts.Status) + return nil + } + + var responseBytes []byte + + // Handle already encoded responses(text, json...) + encodedResp, ok := response.Data.([]byte) + if ok { + responseBytes = encodedResp + } else { + if responseBytes, err = xml.Marshal(response.Data); err != nil { + return err + } + + if len(responseBytes) > 0 { + ctx.Response().Header.Set("Content-Length", fmt.Sprint(len(responseBytes))) + ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML) + } + } + + if s3logger != nil { + s3logger.Log(ctx, nil, responseBytes, s3log.LogMeta{ + Action: opts.Action, + BucketOwner: opts.BucketOwner, + ObjectSize: opts.ObjectSize, + }) + } + + if s3evnt != nil { + s3evnt.SendEvent(ctx, s3event.EventMeta{ + BucketOwner: opts.BucketOwner, + ObjectSize: opts.ObjectSize, + ObjectETag: opts.ObjectETag, + VersionId: opts.VersionId, + EventName: opts.EventName, + }) + } + + if ok { + if len(responseBytes) > 0 { + ctx.Response().Header.Set("Content-Length", fmt.Sprint(len(responseBytes))) + } + + return ctx.Send(responseBytes) + } + + msglen := len(xmlhdr) + len(responseBytes) + if msglen > maxXMLBodyLen { + debuglogger.Logf("XML encoded body len %v exceeds max len %v", + msglen, maxXMLBodyLen) + ctx.Status(http.StatusInternalServerError) + + return ctx.Send(s3err.GetAPIErrorResponse( + s3err.GetAPIError(s3err.ErrInternalError), "", "", "")) + } + res := make([]byte, 0, msglen) + res = append(res, xmlhdr...) + res = append(res, responseBytes...) + + return ctx.Send(res) + } +} + +func SetResponseHeaders(ctx *fiber.Ctx, headers map[string]string) { + if headers == nil { + return + } + for key, val := range headers { + ctx.Response().Header.Add(key, val) + } +} diff --git a/s3api/middlewares/router-utilities.go b/s3api/middlewares/router-utilities.go new file mode 100644 index 00000000..2a09f251 --- /dev/null +++ b/s3api/middlewares/router-utilities.go @@ -0,0 +1,49 @@ +// 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 middlewares + +import ( + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/s3api/utils" +) + +func MatchQueryArgs(args ...string) fiber.Handler { + return func(ctx *fiber.Ctx) error { + if utils.ContextKeySkip.IsSet(ctx) { + return ctx.Next() + } + for _, query := range args { + if !ctx.Request().URI().QueryArgs().Has(query) { + utils.ContextKeySkip.Set(ctx, true) + break + } + } + return ctx.Next() + } +} + +func MatchQueryArgWithValue(key, val string) fiber.Handler { + return func(ctx *fiber.Ctx) error { + if utils.ContextKeySkip.IsSet(ctx) { + return ctx.Next() + } + + if ctx.Query(key) != val { + utils.ContextKeySkip.Set(ctx, true) + } + + return ctx.Next() + } +} diff --git a/s3api/router.go b/s3api/router.go index f640cb67..0a60029a 100644 --- a/s3api/router.go +++ b/s3api/router.go @@ -67,11 +67,17 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ // HeadBucket app.Head("/:bucket", s3ApiController.HeadBucket) - // GetBucketAcl action - // ListMultipartUploads action - // ListObjects action - // ListObjectsV2 action - app.Get("/:bucket", s3ApiController.ListActions) + app.Get("/:bucket", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(s3ApiController.GetBucketTagging, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(s3ApiController.GetBucketOwnershipControls, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(s3ApiController.GetBucketVersioning, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(s3ApiController.GetBucketPolicy, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(s3ApiController.GetBucketCors, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(s3ApiController.GetObjectLockConfiguration, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(s3ApiController.GetBucketAcl, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("uploads"), controllers.ProcessResponse(s3ApiController.ListMultipartUploads, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgs("versions"), controllers.ProcessResponse(s3ApiController.ListObjectVersions, logger, evs, mm)) + app.Get("/:bucket", middlewares.MatchQueryArgWithValue("list-type", "2"), controllers.ProcessResponse(s3ApiController.ListObjectsV2, logger, evs, mm)) + app.Get("/:bucket", controllers.ProcessResponse(s3ApiController.ListObjects, logger, evs, mm)) // HeadObject action app.Head("/:bucket/:key/*", s3ApiController.HeadObject) diff --git a/s3api/utils/context-keys.go b/s3api/utils/context-keys.go index 313248aa..2157240b 100644 --- a/s3api/utils/context-keys.go +++ b/s3api/utils/context-keys.go @@ -34,6 +34,7 @@ const ( ContextKeyParsedAcl ContextKey = "parsed-acl" ContextKeySkipResBodyLog ContextKey = "skip-res-body-log" ContextKeyBodyReader ContextKey = "body-reader" + ContextKeySkip ContextKey = "__skip" ) func (ck ContextKey) Values() []ContextKey { @@ -60,6 +61,10 @@ func (ck ContextKey) IsSet(ctx *fiber.Ctx) bool { return val != nil } +func (ck ContextKey) Delete(ctx *fiber.Ctx) { + ctx.Locals(string(ck), nil) +} + func (ck ContextKey) Get(ctx *fiber.Ctx) any { return ctx.Locals(string(ck)) }