Files
versitygw/s3api/controllers/bucket-put_test.go
niksis02 7744dacced fix: adds validation for bucket canned ACL
Fixes #1379

Adds validation for bucket canned ACLs in `CreateBucket` and `PutBucketAcl`. The gateway supports three values: `private`, `public-read`, and `public-read-write`. All other values (including `authenticated-read`, which is not supported) are considered invalid and result in an `InvalidArgument` error with an empty error message.
2025-11-03 22:59:06 +04:00

1210 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.TaggingInput{
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)
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: "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: []string{"*"},
AllowedMethods: []auth.CORSHTTPMethod{http.MethodPost},
},
},
})
assert.NoError(t, err)
invalidCors, err := xml.Marshal(auth.CORSConfiguration{
Rules: []auth.CORSRule{
{
AllowedOrigins: []string{"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"},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "success",
input: testInput{
locals: defaultLocals,
body: []byte(validPolicyDocument),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
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: "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.GetAPIError(s3err.ErrInvalidBucketName),
},
},
{
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.GetAPIError(s3err.ErrInvalidArgument),
},
},
{
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.GetAPIError(s3err.ErrInvalidLocationConstraint),
},
},
{
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.APIError{
Code: "InvalidArgument",
Description: "Invalid x-amz-object-ownership header: invalid_ownership",
HTTPStatusCode: http.StatusBadRequest,
},
},
},
{
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,
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: adminAcc.Access,
},
},
},
},
}
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.GetAPIError(s3err.ErrInvalidArgument),
},
},
{
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,
})
})
}
}