From e6852b3a9957beab302ba4c2315848785928545b Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Wed, 28 Feb 2024 09:48:05 -0500 Subject: [PATCH] feat: Closes 417, Added the following versioning related actions: PutBucketVersioning, GetBucketVersioning, ListObjectVersions. Added versionId support in FE for the following actions: GetObject, DeleteObject --- auth/acl.go | 20 ++++ backend/backend.go | 15 ++- s3api/controllers/backend_moq_test.go | 150 ++++++++++++++++++++++++++ s3api/controllers/base.go | 120 ++++++++++++++++++++- s3api/controllers/base_test.go | 56 +++++++++- s3api/middlewares/acl-parser.go | 7 +- 6 files changed, 359 insertions(+), 9 deletions(-) diff --git a/auth/acl.go b/auth/acl.go index 172b42d5..d10a82f9 100644 --- a/auth/acl.go +++ b/auth/acl.go @@ -250,3 +250,23 @@ func MayCreateBucket(acct Account, isRoot bool) error { return nil } + +func IsAdminOrOwner(acct Account, isRoot bool, acl ACL) error { + // Owner check + if acct.Access == acl.Owner { + return nil + } + + // Root user has access over almost everything + if isRoot { + return nil + } + + // Admin user case + if acct.Role == RoleAdmin { + return nil + } + + // Return access denied in all other cases + return s3err.GetAPIError(s3err.ErrAccessDenied) +} diff --git a/backend/backend.go b/backend/backend.go index 83534714..c91b6eab 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -38,6 +38,8 @@ type Backend interface { CreateBucket(_ context.Context, _ *s3.CreateBucketInput, defaultACL []byte) error PutBucketAcl(_ context.Context, bucket string, data []byte) error DeleteBucket(context.Context, *s3.DeleteBucketInput) error + PutBucketVersioning(context.Context, *s3.PutBucketVersioningInput) error + GetBucketVersioning(_ context.Context, bucket string) (*s3.GetBucketVersioningOutput, error) // multipart operations CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) @@ -60,6 +62,7 @@ type Backend interface { DeleteObject(context.Context, *s3.DeleteObjectInput) error DeleteObjects(context.Context, *s3.DeleteObjectsInput) (s3response.DeleteObjectsResult, error) PutObjectAcl(context.Context, *s3.PutObjectAclInput) error + ListObjectVersions(context.Context, *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) // special case object operations RestoreObject(context.Context, *s3.RestoreObjectInput) error @@ -70,7 +73,7 @@ type Backend interface { PutBucketTagging(_ context.Context, bucket string, tags map[string]string) error DeleteBucketTagging(_ context.Context, bucket string) error - // object tags operations + // object tagging operations GetObjectTagging(_ context.Context, bucket, object string) (map[string]string, error) PutObjectTagging(_ context.Context, bucket, object string, tags map[string]string) error DeleteObjectTagging(_ context.Context, bucket, object string) error @@ -109,6 +112,12 @@ func (BackendUnsupported) PutBucketAcl(_ context.Context, bucket string, data [] func (BackendUnsupported) DeleteBucket(context.Context, *s3.DeleteBucketInput) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } +func (BackendUnsupported) PutBucketVersioning(context.Context, *s3.PutBucketVersioningInput) error { + return s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) GetBucketVersioning(_ context.Context, bucket string) (*s3.GetBucketVersioningOutput, error) { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) +} func (BackendUnsupported) CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) @@ -184,6 +193,10 @@ func (BackendUnsupported) SelectObjectContent(ctx context.Context, input *s3.Sel } } +func (BackendUnsupported) ListObjectVersions(context.Context, *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) +} + func (BackendUnsupported) GetBucketTagging(_ context.Context, bucket string) (map[string]string, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) } diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index 41cfc154..65000467 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -62,6 +62,9 @@ var _ backend.Backend = &BackendMock{} // GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) { // panic("mock out the GetBucketTagging method") // }, +// GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (*s3.GetBucketVersioningOutput, error) { +// panic("mock out the GetBucketVersioning method") +// }, // GetObjectFunc: func(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput, writer io.Writer) (*s3.GetObjectOutput, error) { // panic("mock out the GetObject method") // }, @@ -89,6 +92,9 @@ var _ backend.Backend = &BackendMock{} // ListMultipartUploadsFunc: func(contextMoqParam context.Context, listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) { // panic("mock out the ListMultipartUploads method") // }, +// ListObjectVersionsFunc: func(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) { +// panic("mock out the ListObjectVersions method") +// }, // ListObjectsFunc: func(contextMoqParam context.Context, listObjectsInput *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) { // panic("mock out the ListObjects method") // }, @@ -104,6 +110,9 @@ var _ backend.Backend = &BackendMock{} // PutBucketTaggingFunc: func(contextMoqParam context.Context, bucket string, tags map[string]string) error { // panic("mock out the PutBucketTagging method") // }, +// PutBucketVersioningFunc: func(contextMoqParam context.Context, putBucketVersioningInput *s3.PutBucketVersioningInput) error { +// panic("mock out the PutBucketVersioning method") +// }, // PutObjectFunc: func(contextMoqParam context.Context, putObjectInput *s3.PutObjectInput) (string, error) { // panic("mock out the PutObject method") // }, @@ -177,6 +186,9 @@ type BackendMock struct { // GetBucketTaggingFunc mocks the GetBucketTagging method. GetBucketTaggingFunc func(contextMoqParam context.Context, bucket string) (map[string]string, error) + // GetBucketVersioningFunc mocks the GetBucketVersioning method. + GetBucketVersioningFunc func(contextMoqParam context.Context, bucket string) (*s3.GetBucketVersioningOutput, error) + // GetObjectFunc mocks the GetObject method. GetObjectFunc func(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput, writer io.Writer) (*s3.GetObjectOutput, error) @@ -204,6 +216,9 @@ type BackendMock struct { // ListMultipartUploadsFunc mocks the ListMultipartUploads method. ListMultipartUploadsFunc func(contextMoqParam context.Context, listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) + // ListObjectVersionsFunc mocks the ListObjectVersions method. + ListObjectVersionsFunc func(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) + // ListObjectsFunc mocks the ListObjects method. ListObjectsFunc func(contextMoqParam context.Context, listObjectsInput *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) @@ -219,6 +234,9 @@ type BackendMock struct { // PutBucketTaggingFunc mocks the PutBucketTagging method. PutBucketTaggingFunc func(contextMoqParam context.Context, bucket string, tags map[string]string) error + // PutBucketVersioningFunc mocks the PutBucketVersioning method. + PutBucketVersioningFunc func(contextMoqParam context.Context, putBucketVersioningInput *s3.PutBucketVersioningInput) error + // PutObjectFunc mocks the PutObject method. PutObjectFunc func(contextMoqParam context.Context, putObjectInput *s3.PutObjectInput) (string, error) @@ -345,6 +363,13 @@ type BackendMock struct { // Bucket is the bucket argument value. Bucket string } + // GetBucketVersioning holds details about calls to the GetBucketVersioning method. + GetBucketVersioning []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // Bucket is the bucket argument value. + Bucket string + } // GetObject holds details about calls to the GetObject method. GetObject []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -412,6 +437,13 @@ type BackendMock struct { // ListMultipartUploadsInput is the listMultipartUploadsInput argument value. ListMultipartUploadsInput *s3.ListMultipartUploadsInput } + // ListObjectVersions holds details about calls to the ListObjectVersions method. + ListObjectVersions []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // ListObjectVersionsInput is the listObjectVersionsInput argument value. + ListObjectVersionsInput *s3.ListObjectVersionsInput + } // ListObjects holds details about calls to the ListObjects method. ListObjects []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -451,6 +483,13 @@ type BackendMock struct { // Tags is the tags argument value. Tags map[string]string } + // PutBucketVersioning holds details about calls to the PutBucketVersioning method. + PutBucketVersioning []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // PutBucketVersioningInput is the putBucketVersioningInput argument value. + PutBucketVersioningInput *s3.PutBucketVersioningInput + } // PutObject holds details about calls to the PutObject method. PutObject []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -524,6 +563,7 @@ type BackendMock struct { lockDeleteObjects sync.RWMutex lockGetBucketAcl sync.RWMutex lockGetBucketTagging sync.RWMutex + lockGetBucketVersioning sync.RWMutex lockGetObject sync.RWMutex lockGetObjectAcl sync.RWMutex lockGetObjectAttributes sync.RWMutex @@ -533,11 +573,13 @@ type BackendMock struct { lockListBuckets sync.RWMutex lockListBucketsAndOwners sync.RWMutex lockListMultipartUploads sync.RWMutex + lockListObjectVersions sync.RWMutex lockListObjects sync.RWMutex lockListObjectsV2 sync.RWMutex lockListParts sync.RWMutex lockPutBucketAcl sync.RWMutex lockPutBucketTagging sync.RWMutex + lockPutBucketVersioning sync.RWMutex lockPutObject sync.RWMutex lockPutObjectAcl sync.RWMutex lockPutObjectTagging sync.RWMutex @@ -1029,6 +1071,42 @@ func (mock *BackendMock) GetBucketTaggingCalls() []struct { return calls } +// GetBucketVersioning calls GetBucketVersioningFunc. +func (mock *BackendMock) GetBucketVersioning(contextMoqParam context.Context, bucket string) (*s3.GetBucketVersioningOutput, error) { + if mock.GetBucketVersioningFunc == nil { + panic("BackendMock.GetBucketVersioningFunc: method is nil but Backend.GetBucketVersioning was just called") + } + callInfo := struct { + ContextMoqParam context.Context + Bucket string + }{ + ContextMoqParam: contextMoqParam, + Bucket: bucket, + } + mock.lockGetBucketVersioning.Lock() + mock.calls.GetBucketVersioning = append(mock.calls.GetBucketVersioning, callInfo) + mock.lockGetBucketVersioning.Unlock() + return mock.GetBucketVersioningFunc(contextMoqParam, bucket) +} + +// GetBucketVersioningCalls gets all the calls that were made to GetBucketVersioning. +// Check the length with: +// +// len(mockedBackend.GetBucketVersioningCalls()) +func (mock *BackendMock) GetBucketVersioningCalls() []struct { + ContextMoqParam context.Context + Bucket string +} { + var calls []struct { + ContextMoqParam context.Context + Bucket string + } + mock.lockGetBucketVersioning.RLock() + calls = mock.calls.GetBucketVersioning + mock.lockGetBucketVersioning.RUnlock() + return calls +} + // GetObject calls GetObjectFunc. func (mock *BackendMock) GetObject(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput, writer io.Writer) (*s3.GetObjectOutput, error) { if mock.GetObjectFunc == nil { @@ -1361,6 +1439,42 @@ func (mock *BackendMock) ListMultipartUploadsCalls() []struct { return calls } +// ListObjectVersions calls ListObjectVersionsFunc. +func (mock *BackendMock) ListObjectVersions(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) { + if mock.ListObjectVersionsFunc == nil { + panic("BackendMock.ListObjectVersionsFunc: method is nil but Backend.ListObjectVersions was just called") + } + callInfo := struct { + ContextMoqParam context.Context + ListObjectVersionsInput *s3.ListObjectVersionsInput + }{ + ContextMoqParam: contextMoqParam, + ListObjectVersionsInput: listObjectVersionsInput, + } + mock.lockListObjectVersions.Lock() + mock.calls.ListObjectVersions = append(mock.calls.ListObjectVersions, callInfo) + mock.lockListObjectVersions.Unlock() + return mock.ListObjectVersionsFunc(contextMoqParam, listObjectVersionsInput) +} + +// ListObjectVersionsCalls gets all the calls that were made to ListObjectVersions. +// Check the length with: +// +// len(mockedBackend.ListObjectVersionsCalls()) +func (mock *BackendMock) ListObjectVersionsCalls() []struct { + ContextMoqParam context.Context + ListObjectVersionsInput *s3.ListObjectVersionsInput +} { + var calls []struct { + ContextMoqParam context.Context + ListObjectVersionsInput *s3.ListObjectVersionsInput + } + mock.lockListObjectVersions.RLock() + calls = mock.calls.ListObjectVersions + mock.lockListObjectVersions.RUnlock() + return calls +} + // ListObjects calls ListObjectsFunc. func (mock *BackendMock) ListObjects(contextMoqParam context.Context, listObjectsInput *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) { if mock.ListObjectsFunc == nil { @@ -1549,6 +1663,42 @@ func (mock *BackendMock) PutBucketTaggingCalls() []struct { return calls } +// PutBucketVersioning calls PutBucketVersioningFunc. +func (mock *BackendMock) PutBucketVersioning(contextMoqParam context.Context, putBucketVersioningInput *s3.PutBucketVersioningInput) error { + if mock.PutBucketVersioningFunc == nil { + panic("BackendMock.PutBucketVersioningFunc: method is nil but Backend.PutBucketVersioning was just called") + } + callInfo := struct { + ContextMoqParam context.Context + PutBucketVersioningInput *s3.PutBucketVersioningInput + }{ + ContextMoqParam: contextMoqParam, + PutBucketVersioningInput: putBucketVersioningInput, + } + mock.lockPutBucketVersioning.Lock() + mock.calls.PutBucketVersioning = append(mock.calls.PutBucketVersioning, callInfo) + mock.lockPutBucketVersioning.Unlock() + return mock.PutBucketVersioningFunc(contextMoqParam, putBucketVersioningInput) +} + +// PutBucketVersioningCalls gets all the calls that were made to PutBucketVersioning. +// Check the length with: +// +// len(mockedBackend.PutBucketVersioningCalls()) +func (mock *BackendMock) PutBucketVersioningCalls() []struct { + ContextMoqParam context.Context + PutBucketVersioningInput *s3.PutBucketVersioningInput +} { + var calls []struct { + ContextMoqParam context.Context + PutBucketVersioningInput *s3.PutBucketVersioningInput + } + mock.lockPutBucketVersioning.RLock() + calls = mock.calls.PutBucketVersioning + mock.lockPutBucketVersioning.RUnlock() + return calls +} + // PutObject calls PutObjectFunc. func (mock *BackendMock) PutObject(contextMoqParam context.Context, putObjectInput *s3.PutObjectInput) (string, error) { if mock.PutObjectFunc == nil { diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 14836fc4..6faba16f 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -74,6 +74,7 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + versionId := ctx.Query("versionId") if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") } @@ -229,9 +230,10 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { ctx.Locals("logResBody", false) res, err := c.be.GetObject(ctx.Context(), &s3.GetObjectInput{ - Bucket: &bucket, - Key: &key, - Range: &acceptRange, + Bucket: &bucket, + Key: &key, + Range: &acceptRange, + VersionId: &versionId, }, ctx.Response().BodyWriter()) if err != nil { return SendResponse(ctx, err, @@ -333,6 +335,7 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { keyMarker := ctx.Query("key-marker") maxUploadsStr := ctx.Query("max-uploads") uploadIdMarker := ctx.Query("upload-id-marker") + versionIdMarker := ctx.Query("version-id-marker") acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) @@ -374,6 +377,72 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("versioning") { + err := auth.VerifyACL(parsedAcl, acct.Access, "READ", isRoot) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetBucketVersioning", + 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, + Action: "GetBucketVersioning", + BucketOwner: parsedAcl.Owner, + }) + } + + data, err := c.be.GetBucketVersioning(ctx.Context(), bucket) + return SendXMLResponse(ctx, data, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetBucketVersioning", + BucketOwner: parsedAcl.Owner, + }) + } + + if ctx.Request().URI().QueryArgs().Has("versions") { + err := auth.VerifyACL(parsedAcl, acct.Access, "READ", isRoot) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "ListObjectVersions", + BucketOwner: parsedAcl.Owner, + }) + } + + maxkeys, err := utils.ParseUint(maxkeysStr) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "ListObjectVersions", + 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, + Action: "ListObjectVersions", + BucketOwner: parsedAcl.Owner, + }) + } + if ctx.Request().URI().QueryArgs().Has("acl") { err := auth.VerifyACL(parsedAcl, acct.Access, "READ_ACP", isRoot) if err != nil { @@ -518,6 +587,8 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp") granWrite := ctx.Get("X-Amz-Grant-Write") grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp") + mfa := ctx.Get("X-Amz-Mfa") + contentMD5 := ctx.Get("Content-MD5") acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) @@ -568,6 +639,43 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("versioning") { + parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + err := auth.VerifyACL(parsedAcl, acct.Access, "WRITE", isRoot) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutBucketVersioning", + BucketOwner: parsedAcl.Owner, + }) + } + + var versioningConf types.VersioningConfiguration + err = xml.Unmarshal(ctx.Body(), &versioningConf) + if err != nil { + return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), + &MetaOpts{ + Logger: c.logger, + Action: "PutBucketVersioning", + BucketOwner: parsedAcl.Owner, + }) + } + + err = c.be.PutBucketVersioning(ctx.Context(), &s3.PutBucketVersioningInput{ + Bucket: &bucket, + MFA: &mfa, + VersioningConfiguration: &versioningConf, + ContentMD5: &contentMD5, + }) + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + Action: "PutBucketVersioning", + BucketOwner: parsedAcl.Owner, + }) + } + grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP if ctx.Request().URI().QueryArgs().Has("acl") { @@ -1218,6 +1326,7 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { key := ctx.Params("key") keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") + versionId := ctx.Query("versionId") acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) @@ -1291,8 +1400,9 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { err = c.be.DeleteObject(ctx.Context(), &s3.DeleteObjectInput{ - Bucket: &bucket, - Key: &key, + Bucket: &bucket, + Key: &key, + VersionId: &versionId, }) return SendResponse(ctx, err, &MetaOpts{ diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index e694afd2..9e4c9ba3 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -346,6 +346,12 @@ func TestS3ApiController_ListActions(t *testing.T) { GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) { return map[string]string{}, nil }, + GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (*s3.GetBucketVersioningOutput, error) { + return &s3.GetBucketVersioningOutput{}, nil + }, + ListObjectVersionsFunc: func(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) { + return &s3.ListObjectVersionsOutput{}, nil + }, }, } @@ -453,6 +459,24 @@ func TestS3ApiController_ListActions(t *testing.T) { 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-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) { @@ -527,6 +551,13 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { ` + versioningBody := ` + + Enabled + Enabled + + ` + s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { @@ -541,6 +572,9 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { PutBucketTaggingFunc: func(contextMoqParam context.Context, bucket string, tags map[string]string) error { return nil }, + PutBucketVersioningFunc: func(contextMoqParam context.Context, putBucketVersioningInput *s3.PutBucketVersioningInput) error { + return nil + }, }, } // Mock ctx.Locals @@ -599,6 +633,24 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { wantErr: false, statusCode: 200, }, + { + name: "Put-bucket-versioning-invalid-body", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket?versioning", nil), + }, + wantErr: false, + statusCode: 400, + }, + { + name: "Put-bucket-versioning-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPut, "/my-bucket?versioning", strings.NewReader(versioningBody)), + }, + wantErr: false, + statusCode: 200, + }, { name: "Put-bucket-acl-invalid-acl", app: app, @@ -906,12 +958,12 @@ func TestS3ApiController_PutActions(t *testing.T) { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.GetActions() %v error = %v, wantErr %v", + t.Errorf("S3ApiController.PutActions() %v error = %v, wantErr %v", tt.name, err, tt.wantErr) } if resp.StatusCode != tt.statusCode { - t.Errorf("S3ApiController.GetActions() %v statusCode = %v, wantStatusCode = %v", + t.Errorf("S3ApiController.PutActions() %v statusCode = %v, wantStatusCode = %v", tt.name, resp.StatusCode, tt.statusCode) } } diff --git a/s3api/middlewares/acl-parser.go b/s3api/middlewares/acl-parser.go index 7557d3db..ee53028f 100644 --- a/s3api/middlewares/acl-parser.go +++ b/s3api/middlewares/acl-parser.go @@ -38,7 +38,12 @@ func AclParser(be backend.Backend, logger s3log.AuditLogger) fiber.Handler { if ctx.Method() == http.MethodPatch { return ctx.Next() } - if len(pathParts) == 2 && pathParts[1] != "" && ctx.Method() == http.MethodPut && !ctx.Request().URI().QueryArgs().Has("acl") && !ctx.Request().URI().QueryArgs().Has("tagging") { + if len(pathParts) == 2 && + pathParts[1] != "" && + ctx.Method() == http.MethodPut && + !ctx.Request().URI().QueryArgs().Has("acl") && + !ctx.Request().URI().QueryArgs().Has("tagging") && + !ctx.Request().URI().QueryArgs().Has("versioning") { if err := auth.MayCreateBucket(acct, isRoot); err != nil { return controllers.SendXMLResponse(ctx, nil, err, &controllers.MetaOpts{Logger: logger, Action: "CreateBucket"}) }