Files
versitygw/s3api/controllers/bucket-put_test.go
niksis02 4ef090dbfc fix: fix empty ownership control rules panic
Fix the gateway panic when validating malformed bucket ownership controls bodies with no rules. The handler now checks the rules count before indexing the first rule.
2026-05-23 04:18:06 +04:00

1233 lines
28 KiB
Go

// 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 (
"context"
"encoding/xml"
"errors"
"fmt"
"net/http"
"testing"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
func TestS3ApiController_PutBucketTagging(t *testing.T) {
validTaggingBody, err := xml.Marshal(s3response.Tagging{
TagSet: s3response.TagSet{
Tags: []s3response.Tag{
{
Key: "key",
Value: "val",
},
},
},
})
assert.NoError(t, err)
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "verify access fails",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "invalid request body",
input: testInput{
locals: defaultLocals,
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "backend returns error",
input: testInput{
locals: defaultLocals,
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
body: validTaggingBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
Status: http.StatusNoContent,
},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "successful response",
input: testInput{
locals: defaultLocals,
body: validTaggingBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
Status: http.StatusNoContent,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
PutBucketTaggingFunc: func(contextMoqParam context.Context, bucket string, tags map[string]string) error {
return tt.input.beErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
}
testController(
t,
ctrl.PutBucketTagging,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_PutBucketOwnershipControls(t *testing.T) {
validOwnershipBody, err := xml.Marshal(
s3response.OwnershipControls{
Rules: []types.OwnershipControlsRule{
{ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced},
},
})
assert.NoError(t, err)
invalidRuleCountBody, err := xml.Marshal(
s3response.OwnershipControls{
Rules: []types.OwnershipControlsRule{
{ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced},
{ObjectOwnership: types.ObjectOwnershipBucketOwnerPreferred},
},
},
)
assert.NoError(t, err)
emptyRulesBody, err := xml.Marshal(
s3response.OwnershipControls{},
)
assert.NoError(t, err)
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "verify access fails",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "invalid request body",
input: testInput{
locals: defaultLocals,
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "invalid rules count",
input: testInput{
locals: defaultLocals,
body: invalidRuleCountBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "empty rules count",
input: testInput{
locals: defaultLocals,
body: emptyRulesBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "backend error",
input: testInput{
locals: defaultLocals,
body: validOwnershipBody,
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "success",
input: testInput{
locals: defaultLocals,
body: validOwnershipBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
PutBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error {
return tt.input.beErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
}
testController(t, ctrl.PutBucketOwnershipControls, tt.output.response, tt.output.err, ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_PutBucketVersioning(t *testing.T) {
validVersioningBody, err := xml.Marshal(
types.VersioningConfiguration{
Status: types.BucketVersioningStatusEnabled,
},
)
assert.NoError(t, err)
invalidVersioningStatusBody, err := xml.Marshal(
types.VersioningConfiguration{
Status: types.BucketVersioningStatus("invalid_status"),
},
)
assert.NoError(t, err)
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "verify access fails",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "invalid request body",
input: testInput{
locals: defaultLocals,
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrInvalidRequest),
},
},
{
name: "invalid rules count",
input: testInput{
locals: defaultLocals,
body: invalidVersioningStatusBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "backend error",
input: testInput{
locals: defaultLocals,
body: validVersioningBody,
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "success",
input: testInput{
locals: defaultLocals,
body: validVersioningBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
PutBucketVersioningFunc: func(contextMoqParam context.Context, bucket string, status types.BucketVersioningStatus) error {
return tt.input.beErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
}
testController(t, ctrl.PutBucketVersioning, tt.output.response, tt.output.err, ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_PutObjectLockConfiguration(t *testing.T) {
validLockBody, err := xml.Marshal(
types.ObjectLockConfiguration{
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
},
)
assert.NoError(t, err)
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "verify access fails",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "invalid request body",
input: testInput{
locals: defaultLocals,
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "backend error",
input: testInput{
locals: defaultLocals,
body: validLockBody,
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "success",
input: testInput{
locals: defaultLocals,
body: validLockBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string, config []byte) error {
return tt.input.beErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
}
testController(t, ctrl.PutObjectLockConfiguration, tt.output.response, tt.output.err, ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_PutBucketCors(t *testing.T) {
validBody, err := xml.Marshal(auth.CORSConfiguration{
Rules: []auth.CORSRule{
{
AllowedOrigins: []auth.CORSOrigin{"*"},
AllowedMethods: []auth.CORSHTTPMethod{http.MethodPost},
},
},
})
assert.NoError(t, err)
invalidCors, err := xml.Marshal(auth.CORSConfiguration{
Rules: []auth.CORSRule{
{
AllowedOrigins: []auth.CORSOrigin{"origin"},
AllowedMethods: []auth.CORSHTTPMethod{"invalid_method"},
},
},
})
assert.NoError(t, err)
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "verify access fails",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "invalid request body",
input: testInput{
locals: defaultLocals,
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "invalid cors config",
input: testInput{
locals: defaultLocals,
body: invalidCors,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetUnsopportedCORSMethodErr("invalid_method"),
},
},
{
name: "backend error",
input: testInput{
locals: defaultLocals,
beErr: s3err.GetAPIError(s3err.ErrNotImplemented),
body: validBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.GetAPIError(s3err.ErrNotImplemented),
},
},
{
name: "success",
input: testInput{
locals: defaultLocals,
body: validBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
PutBucketCorsFunc: func(contextMoqParam context.Context, bucket string, cors []byte) error {
return tt.input.beErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
}
testController(t, ctrl.PutBucketCors, tt.output.response, tt.output.err, ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
headers: tt.input.headers,
})
})
}
}
func TestS3ApiController_PutBucketPolicy(t *testing.T) {
validPolicyDocument :=
`{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket/*"
}
]
}`
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "verify access fails",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "invalid policy document",
input: testInput{
locals: defaultLocals,
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: "root"},
},
err: s3err.APIError{
Code: "MalformedPolicy",
Description: "Policies must be valid JSON and the first byte must be '{'",
HTTPStatusCode: http.StatusBadRequest,
},
},
},
{
name: "backend error",
input: testInput{
locals: defaultLocals,
body: []byte(validPolicyDocument),
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
Status: http.StatusNoContent,
},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "success",
input: testInput{
locals: defaultLocals,
body: []byte(validPolicyDocument),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
Status: http.StatusNoContent,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
PutBucketPolicyFunc: func(contextMoqParam context.Context, bucket string, policy []byte) error {
return tt.input.beErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
}
testController(t, ctrl.PutBucketPolicy, tt.output.response, tt.output.err, ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_CreateBucket(t *testing.T) {
adminAcc := auth.Account{
Access: "root",
Role: auth.RoleAdmin,
}
userAcc := auth.Account{
Access: "user",
Role: auth.RoleUser,
}
invLocConstBody, err := xml.Marshal(s3response.CreateBucketConfiguration{
LocationConstraint: utils.GetStringPtr("us-west-1"),
})
assert.NoError(t, err)
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "access denied",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: userAcc,
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "invalid bucket name",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
bucket: "invalid_bucket_name",
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: adminAcc.Access,
},
},
err: s3err.GetBucketErr(s3err.ErrInvalidBucketName, "invalid_bucket_name"),
},
},
{
name: "malformed body",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
},
{
name: "invalid canned acl",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
headers: map[string]string{
"x-amz-acl": "invalid_acl",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgCannedAcl, "invalid_acl"),
},
},
{
name: "invalid location constraint",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
utils.ContextKeyRegion: "us-east-1",
},
body: invLocConstBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
},
err: s3err.GetInvalidLocationConstraintErr("us-west-1"),
},
},
{
name: "invalid ownership",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
headers: map[string]string{
"X-Amz-Object-Ownership": "invalid_ownership",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: adminAcc.Access,
},
},
err: s3err.GetInvalidArgObjectOwnership("invalid_ownership"),
},
},
{
name: "invalid ownership + acl",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
headers: map[string]string{
"X-Amz-Object-Ownership": string(types.ObjectOwnershipBucketOwnerEnforced),
"X-Amz-Acl": string(types.BucketCannedACLPublicRead),
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
},
err: s3err.GetAPIError(s3err.ErrInvalidBucketAclWithObjectOwnership),
},
},
{
name: "both grants and canned acl",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
headers: map[string]string{
"X-Amz-Acl": string(types.BucketCannedACLPublicRead),
"X-Amz-Grant-Read": userAcc.Access,
"X-Amz-Object-Ownership": string(types.ObjectOwnershipBucketOwnerPreferred),
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
},
err: s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
},
},
{
name: "fail to update the acl",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
headers: map[string]string{
"X-Amz-Grant-Read": userAcc.Access,
"X-Amz-Object-Ownership": string(types.ObjectOwnershipBucketOwnerPreferred),
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
},
err: fmt.Errorf("accounts does not exist: %s", userAcc.Access),
},
},
{
name: "backend error",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "success",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyAccount: adminAcc,
},
bucket: "my-bucket",
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: adminAcc.Access,
},
Headers: map[string]*string{
"Location": utils.GetStringPtr("/my-bucket"),
"x-amz-bucket-arn": utils.GetStringPtr("arn:aws:s3:::my-bucket"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
CreateBucketFunc: func(contextMoqParam context.Context, createBucketInput *s3.CreateBucketInput, defaultACL []byte) error {
return tt.input.beErr
},
}
ctrl := S3ApiController{
be: be,
iam: auth.NewIAMServiceSingle(adminAcc),
}
testController(t, ctrl.CreateBucket, tt.output.response, tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
bucket: tt.input.bucket,
headers: tt.input.headers,
})
})
}
}
func TestS3ApiController_PutBucketAcl(t *testing.T) {
invalidBody, err := xml.Marshal(auth.AccessControlPolicy{
Owner: &types.Owner{
ID: utils.GetStringPtr("root"),
},
AccessControlList: auth.AccessControlList{
Grants: []auth.Grant{
{
Permission: auth.Permission("invalid_permission"),
},
},
},
})
assert.NoError(t, err)
incorrectOwnerBody, err := xml.Marshal(auth.AccessControlPolicy{
Owner: &types.Owner{
ID: utils.GetStringPtr("user"),
},
AccessControlList: auth.AccessControlList{},
})
assert.NoError(t, err)
validAccessControlPolicy, err := xml.Marshal(auth.AccessControlPolicy{
Owner: &types.Owner{
ID: utils.GetStringPtr("root"),
},
AccessControlList: auth.AccessControlList{},
})
assert.NoError(t, err)
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "access denied",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "fails to get bucket ownership",
input: testInput{
locals: defaultLocals,
extraMockErr: s3err.GetAPIError(s3err.ErrInternalError),
extraMockResp: types.ObjectOwnership(""),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInternalError),
},
},
{
name: "acl not supported",
input: testInput{
locals: defaultLocals,
extraMockResp: types.ObjectOwnershipBucketOwnerEnforced,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAclNotSupported),
},
},
{
name: "invalid request body",
input: testInput{
locals: defaultLocals,
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
body: []byte("invalid_body"),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrMalformedACL),
},
},
{
name: "invalid access control policy",
input: testInput{
locals: defaultLocals,
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
body: invalidBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrMalformedACL),
},
},
{
name: "incorrect owner id",
input: testInput{
locals: defaultLocals,
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
body: incorrectOwnerBody,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.APIError{
Code: "InvalidArgument",
Description: "Invalid id",
HTTPStatusCode: http.StatusBadRequest,
},
},
},
{
name: "both access control policy and grants",
input: testInput{
body: validAccessControlPolicy,
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
headers: map[string]string{
"X-Amz-Acl": "public-read",
},
locals: defaultLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrUnexpectedContent),
},
},
{
name: "access control policy success",
input: testInput{
body: validAccessControlPolicy,
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
locals: defaultLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
{
name: "invalid canned acl",
input: testInput{
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
headers: map[string]string{
"X-Amz-Acl": "invalid_acl",
},
locals: defaultLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgCannedAcl, "invalid_acl"),
},
},
{
name: "both canned acl and grants",
input: testInput{
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
headers: map[string]string{
"X-Amz-Acl": "public-read",
"X-Amz-Grant-Read": "grt1",
},
locals: defaultLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
},
},
{
name: "canned acl success",
input: testInput{
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
headers: map[string]string{
"X-Amz-Acl": "public-read",
},
locals: defaultLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
{
name: "grants update acl fails",
input: testInput{
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
headers: map[string]string{
"X-Amz-Grant-Read": "grt1",
},
locals: defaultLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: errors.New("accounts does not exist: grt1"),
},
},
{
name: "no option provided",
input: testInput{
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
locals: defaultLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrMissingSecurityHeader),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
PutBucketAclFunc: func(contextMoqParam context.Context, bucket string, data []byte) error {
return tt.input.beErr
},
GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
return tt.input.extraMockResp.(types.ObjectOwnership), tt.input.extraMockErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
iam: auth.NewIAMServiceSingle(
auth.Account{
Access: "root",
}),
}
testController(t, ctrl.PutBucketAcl, tt.output.response, tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
bucket: tt.input.bucket,
headers: tt.input.headers,
})
})
}
}