Compare commits

...
10 Commits
Author SHA1 Message Date
dualfroz 158d7649bd Describe this repository 2026-10-22 21:08:01 +02:00
dualfroz 0e3045b87f fix: drop the owner's FULL_CONTROL grant when grant headers set the bucket ACL
CreateBucket and PutBucketAcl with x-amz-grant-* headers stored the
requested grants plus a FULL_CONTROL grant for the bucket owner, so
GetBucketAcl returned a grant AWS doesn't. On AWS the headers replace the
whole ACL and the owner gets FULL_CONTROL only when a header names it.

UpdateACL now keeps just the header grants when there are any. A canned
ACL and a bucket created without grants still get the owner's FULL_CONTROL
grant. The owner keeps full access either way, since aclAllows lets the
bucket owner perform any action regardless of grants.

Fixes #1407
2026-10-14 19:40:59 +02:00
upstream 3fee7f49c9 Upstream versity/versitygw state of the touched files before 3d4c24e2e1 2026-10-14 19:40:59 +02:00
dualfroz deab0fc412 test: enable the missing Host header ListBuckets test
Since fasthttp v1.70.0 an HTTP/1.1 request without a Host header is
rejected and the gateway answers it with 400 BadRequest. Add a server
test that sends such requests over a raw connection and stop skipping
the REST missing host test against the gateway.

Fixes #1530
2026-10-14 18:55:47 +02:00
upstream cf6c64c93a Upstream versity/versitygw state of the touched files before 1ec04af468 2026-10-14 18:55:47 +02:00
dualfroz 276128b42b fix: reject an empty start-after and continuation-token in ListObjects (V1) 2026-10-12 14:30:16 +02:00
upstream 613c71290f Upstream versity/versitygw state of the touched files before aed4617372 2026-10-12 14:30:16 +02:00
dualfroz 7dce327c11 fix: return NextMarker in ListObjects only when a delimiter is set
Fixes #999
2026-10-10 11:50:51 +02:00
upstream 295bb05dd1 Upstream versity/versitygw state of the touched files before c02173db7a 2026-10-10 11:50:51 +02:00
dualfroz b8628c396f fix: reject the start-after query in ListObjects (V1)
Fixes #2004
2026-10-10 11:46:12 +02:00
14 changed files with 2426 additions and 30 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
# Contributions to versity/versitygw
A simple to deploy but feature rich S3 object storage server for your filesystem
This repository holds only my commits to the upstream project, with their original author, date and diff.
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
## Merged pull requests
- [fix: return NextMarker in ListObjects only when a delimiter is set](https://github.com/versity/versitygw/pull/2506) (2026-10-13)
- [fix: reject the start-after query in ListObjects (V1)](https://github.com/versity/versitygw/pull/2505) (2026-10-13)
- [fix: compute IsTruncated from the absolute index in ListMultipartUploads](https://github.com/versity/versitygw/pull/2475) (2026-10-08)
- [fix: validate and apply the ListBuckets bucket-region query](https://github.com/versity/versitygw/pull/2474) (2026-10-07)
- [fix: drop upload-id-marker without key-marker in ListMultipartUploads](https://github.com/versity/versitygw/pull/2477) (2026-10-05)
- [fix: report the last upload in ListMultipartUploads next markers on a single page](https://github.com/versity/versitygw/pull/2476) (2026-10-05)
- [fix: return InvalidURI for a malformed escape in the request path](https://github.com/versity/versitygw/pull/2473) (2026-10-05)
- [fix: accept upload-id-marker together with key-marker in ListMultipartUploads](https://github.com/versity/versitygw/pull/2472) (2026-10-05)
- [fix: return MalformedXML for a DeleteObjects request without objects](https://github.com/versity/versitygw/pull/2471) (2026-10-05)
- [fix: reject ListMultipartUploads requests that also carry uploadId](https://github.com/versity/versitygw/pull/2470) (2026-10-05)
- [Return IllegalLocationConstraintException for a mismatched LocationConstraint](https://github.com/versity/versitygw/pull/2389) (2026-09-14)
+525
View File
@@ -0,0 +1,525 @@
// 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 auth
import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"slices"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/debuglogger"
"github.com/versity/versitygw/s3err"
)
type ACL struct {
Owner string
Grantees []Grantee
}
// IsPublic specifies if the acl grants public read access
func (acl *ACL) IsPublic(permission Permission) bool {
for _, grt := range acl.Grantees {
if grt.Permission == permission && grt.Type == types.TypeGroup && grt.Access == "all-users" {
return true
}
}
return false
}
type Grantee struct {
Permission Permission
Access string
Type types.Type
}
type GetBucketAclOutput struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ AccessControlPolicy"`
Owner *types.Owner
AccessControlList AccessControlList
}
type PutBucketAclInput struct {
Bucket *string
ACL types.BucketCannedACL
AccessControlPolicy *AccessControlPolicy
GrantFullControl *string
GrantRead *string
GrantReadACP *string
GrantWrite *string
GrantWriteACP *string
}
type AccessControlPolicy struct {
AccessControlList AccessControlList `xml:"AccessControlList"`
Owner *types.Owner
}
func (acp *AccessControlPolicy) Validate() error {
if !acp.AccessControlList.isValid() {
return s3err.GetAPIError(s3err.ErrMalformedACL)
}
// The Owner can't be nil
if acp.Owner == nil {
return s3err.GetAPIError(s3err.ErrMalformedACL)
}
// The Owner ID can't be empty
if acp.Owner.ID == nil || *acp.Owner.ID == "" {
return s3err.GetAPIError(s3err.ErrMalformedACL)
}
return nil
}
type AccessControlList struct {
Grants []Grant `xml:"Grant"`
}
// Validates the AccessControlList
func (acl *AccessControlList) isValid() bool {
for _, el := range acl.Grants {
if !el.isValid() {
return false
}
}
return true
}
type Permission string
const (
PermissionFullControl Permission = "FULL_CONTROL"
PermissionWrite Permission = "WRITE"
PermissionWriteAcp Permission = "WRITE_ACP"
PermissionRead Permission = "READ"
PermissionReadAcp Permission = "READ_ACP"
)
// Check if the permission is valid
func (p Permission) isValid() bool {
return p == PermissionFullControl ||
p == PermissionRead ||
p == PermissionReadAcp ||
p == PermissionWrite ||
p == PermissionWriteAcp
}
type Grant struct {
Grantee *Grt `xml:"Grantee"`
Permission Permission `xml:"Permission"`
}
// Checks if Grant is valid
func (g *Grant) isValid() bool {
return g.Permission.isValid() && g.Grantee.isValid()
}
type Grt struct {
XMLNS string `xml:"xmlns:xsi,attr"`
Type types.Type `xml:"xsi:type,attr"`
ID string `xml:"ID"`
}
// Custom Unmarshalling for Grt to parse xsi:type properly
func (g *Grt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Iterate through the XML tokens to process the attributes
for _, attr := range start.Attr {
// Check if the attribute is xsi:type and belongs to the xsi namespace
if attr.Name.Space == "http://www.w3.org/2001/XMLSchema-instance" && attr.Name.Local == "type" {
g.Type = types.Type(attr.Value)
}
// Handle xmlns:xsi
if attr.Name.Local == "xmlns:xsi" {
g.XMLNS = attr.Value
}
}
// Decode the inner XML elements like ID
for {
t, err := d.Token()
if err != nil {
return err
}
switch se := t.(type) {
case xml.StartElement:
if se.Name.Local == "ID" {
if err := d.DecodeElement(&g.ID, &se); err != nil {
return err
}
}
case xml.EndElement:
if se.Name.Local == start.Name.Local {
return nil
}
}
}
}
// Validates Grt
func (g *Grt) isValid() bool {
// Validate the Type
// Only these 2 types are supported in the gateway
if g.Type != types.TypeCanonicalUser && g.Type != types.TypeGroup {
return false
}
// The ID prop shouldn't be empty
if g.ID == "" {
return false
}
return true
}
func ParseACL(data []byte) (ACL, error) {
if len(data) == 0 {
return ACL{}, nil
}
var acl ACL
if err := json.Unmarshal(data, &acl); err != nil {
return acl, fmt.Errorf("parse acl: %w", err)
}
return acl, nil
}
func ParseACLOutput(data []byte, owner string) (GetBucketAclOutput, error) {
grants := []Grant{}
if len(data) == 0 {
return GetBucketAclOutput{
Owner: &types.Owner{
ID: &owner,
},
AccessControlList: AccessControlList{
Grants: grants,
},
}, nil
}
var acl ACL
if err := json.Unmarshal(data, &acl); err != nil {
return GetBucketAclOutput{}, fmt.Errorf("parse acl: %w", err)
}
for _, elem := range acl.Grantees {
acs := elem.Access
grants = append(grants, Grant{
Grantee: &Grt{
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
ID: acs,
Type: elem.Type,
},
Permission: elem.Permission,
})
}
return GetBucketAclOutput{
Owner: &types.Owner{
ID: &acl.Owner,
},
AccessControlList: AccessControlList{
Grants: grants,
},
}, nil
}
func UpdateACL(input *PutBucketAclInput, acl ACL, iam IAMService) ([]byte, error) {
if input == nil {
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
}
defaultGrantees := []Grantee{
{
Permission: PermissionFullControl,
Access: acl.Owner,
Type: types.TypeCanonicalUser,
},
}
// if the ACL is specified, set the ACL, else replace the grantees
if input.ACL != "" {
switch input.ACL {
case types.BucketCannedACLPublicRead:
defaultGrantees = append(defaultGrantees, Grantee{
Permission: PermissionRead,
Access: "all-users",
Type: types.TypeGroup,
})
case types.BucketCannedACLPublicReadWrite:
defaultGrantees = append(defaultGrantees, []Grantee{
{
Permission: PermissionRead,
Access: "all-users",
Type: types.TypeGroup,
},
{
Permission: PermissionWrite,
Access: "all-users",
Type: types.TypeGroup,
},
}...)
}
} else {
accs := []string{}
if input.GrantRead != nil || input.GrantReadACP != nil || input.GrantFullControl != nil || input.GrantWrite != nil || input.GrantWriteACP != nil {
fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{}
headerGrantees := []Grantee{}
if input.GrantFullControl != nil && *input.GrantFullControl != "" {
fullControlList = splitUnique(*input.GrantFullControl, ",")
for _, str := range fullControlList {
headerGrantees = append(headerGrantees, Grantee{
Access: str,
Permission: PermissionFullControl,
Type: types.TypeCanonicalUser,
})
}
}
if input.GrantRead != nil && *input.GrantRead != "" {
readList = splitUnique(*input.GrantRead, ",")
for _, str := range readList {
headerGrantees = append(headerGrantees, Grantee{
Access: str,
Permission: PermissionRead,
Type: types.TypeCanonicalUser,
})
}
}
if input.GrantReadACP != nil && *input.GrantReadACP != "" {
readACPList = splitUnique(*input.GrantReadACP, ",")
for _, str := range readACPList {
headerGrantees = append(headerGrantees, Grantee{
Access: str,
Permission: PermissionReadAcp,
Type: types.TypeCanonicalUser,
})
}
}
if input.GrantWrite != nil && *input.GrantWrite != "" {
writeList = splitUnique(*input.GrantWrite, ",")
for _, str := range writeList {
headerGrantees = append(headerGrantees, Grantee{
Access: str,
Permission: PermissionWrite,
Type: types.TypeCanonicalUser,
})
}
}
if input.GrantWriteACP != nil && *input.GrantWriteACP != "" {
writeACPList = splitUnique(*input.GrantWriteACP, ",")
for _, str := range writeACPList {
headerGrantees = append(headerGrantees, Grantee{
Access: str,
Permission: PermissionWriteAcp,
Type: types.TypeCanonicalUser,
})
}
}
accs = append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...)
// Grant headers replace the whole ACL, as on AWS, so the owner
// gets no FULL_CONTROL grant unless a header names it. The owner
// keeps full access to its bucket anyway, see aclAllows.
if len(headerGrantees) > 0 {
defaultGrantees = headerGrantees
}
} else {
cache := make(map[string]bool)
for _, grt := range input.AccessControlPolicy.AccessControlList.Grants {
if grt.Grantee == nil || grt.Grantee.ID == "" || grt.Permission == "" {
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
}
access := grt.Grantee.ID
defaultGrantees = append(defaultGrantees, Grantee{
Access: access,
Permission: grt.Permission,
Type: types.TypeCanonicalUser,
})
if _, ok := cache[access]; !ok {
cache[access] = true
accs = append(accs, access)
}
}
}
// Check if the specified accounts exist
accList, err := iam.ResolveAccounts(accs)
if err != nil {
return nil, err
}
if len(accList) > 0 {
return nil, fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", "))
}
}
acl.Grantees = defaultGrantees
result, err := json.Marshal(acl)
if err != nil {
return nil, err
}
return result, nil
}
func splitUnique(s, divider string) []string {
elements := strings.Split(s, divider)
uniqueElements := make(map[string]bool)
result := make([]string, 0, len(elements))
for _, element := range elements {
if _, ok := uniqueElements[element]; !ok {
result = append(result, element)
uniqueElements[element] = true
}
}
return result
}
// aclActionPermissions maps every action an ACL grant can authorize to the
// permission that grants it. The gateway keeps no object ACLs, so the
// bucket's grants stand in for its objects' as well: READ also reads
// objects, and READ_ACP and WRITE_ACP also cover object ACLs. A grant
// authorizes nothing else. Bucket configuration, the bucket policy, tagging
// and object lock settings are the bucket owner's, or whoever a policy
// grants them to. s3:DeleteObjectVersion is left out because WRITE grants
// it to the bucket owner alone. READ reads an object but not its
// attributes, so neither s3:GetObjectAttributes nor
// s3:GetObjectVersionAttributes is here.
var aclActionPermissions = map[Action]Permission{
ListBucketAction: PermissionRead,
ListBucketVersionsAction: PermissionRead,
ListBucketMultipartUploadsAction: PermissionRead,
GetObjectAction: PermissionRead,
GetObjectVersionAction: PermissionRead,
PutObjectAction: PermissionWrite,
DeleteObjectAction: PermissionWrite,
GetBucketAclAction: PermissionReadAcp,
GetObjectAclAction: PermissionReadAcp,
PutBucketAclAction: PermissionWriteAcp,
PutObjectAclAction: PermissionWriteAcp,
}
// aclAllows reports whether acl lets access perform action. The bucket
// owner may perform any action on its own bucket, as an AWS account may.
// Any other grantee may perform only the actions its grants map to, and
// none at all while ACLs are disabled.
func aclAllows(acl ACL, access string, action Action, disableACL bool) bool {
if access == acl.Owner {
return true
}
if disableACL {
return false
}
permission, ok := aclActionPermissions[action]
if !ok {
return false
}
grantee := Grantee{
Access: access,
Permission: permission,
Type: types.TypeCanonicalUser,
}
granteeFullCtrl := Grantee{
Access: access,
Permission: PermissionFullControl,
Type: types.TypeCanonicalUser,
}
granteeAllUsers := Grantee{
Access: "all-users",
Permission: permission,
Type: types.TypeGroup,
}
return slices.ContainsFunc(acl.Grantees, func(grt Grantee) bool {
return grt == grantee || grt == granteeFullCtrl || grt == granteeAllUsers
})
}
// Verifies if the bucket acl grants public access
func VerifyPublicBucketACL(ctx context.Context, be backend.Backend, bucket string, permission Permission) error {
aclBytes, err := be.GetBucketAcl(ctx, &s3.GetBucketAclInput{
Bucket: &bucket,
})
if err != nil {
return err
}
acl, err := ParseACL(aclBytes)
if err != nil {
return err
}
if !acl.IsPublic(permission) {
return errAccessDenied
}
return nil
}
// UpdateBucketACLOwner sets default ACL with new owner and removes
// any previous bucket policy that was in place
func UpdateBucketACLOwner(ctx context.Context, be backend.Backend, bucket, newOwner string) error {
acl := ACL{
Owner: newOwner,
Grantees: []Grantee{
{
Permission: PermissionFullControl,
Access: newOwner,
Type: types.TypeCanonicalUser,
},
},
}
result, err := json.Marshal(acl)
if err != nil {
return fmt.Errorf("marshal ACL: %w", err)
}
err = be.PutBucketAcl(ctx, bucket, result)
if err != nil {
return err
}
return be.DeleteBucketPolicy(ctx, bucket)
}
// ValidateCannedACL validates bucket canned acl value
func ValidateCannedACL(acl types.BucketCannedACL) error {
switch types.BucketCannedACL(acl) {
case types.BucketCannedACLPrivate, types.BucketCannedACLPublicRead, types.BucketCannedACLPublicReadWrite, "":
return nil
default:
debuglogger.Logf("invalid bucket canned acl: %v", acl)
return s3err.GetInvalidArgumentErr(s3err.InvalidArgCannedAcl, string(acl))
}
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright 2026 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 auth
import (
"testing"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
)
// existingAccountsIAM resolves every access key id it is asked about, so
// UpdateACL accepts any grantee.
type existingAccountsIAM struct {
IAMService
}
func (existingAccountsIAM) ResolveAccounts([]string) ([]string, error) {
return nil, nil
}
// TestUpdateACL checks which grants end up in the bucket ACL. Grant headers
// replace the ACL, as on AWS, so the owner gets a FULL_CONTROL grant only
// from a canned ACL, from the default ACL of a bucket created without
// grants, or when a header names it.
func TestUpdateACL(t *testing.T) {
empty := ""
readAcpGrantee := "user1"
ownerGrantee := "owner"
ownerFullControl := Grantee{
Permission: PermissionFullControl,
Access: "owner",
Type: types.TypeCanonicalUser,
}
tests := []struct {
name string
input *PutBucketAclInput
want []Grantee
}{
{
name: "grant header",
input: &PutBucketAclInput{
GrantReadACP: &readAcpGrantee,
},
want: []Grantee{
{
Permission: PermissionReadAcp,
Access: "user1",
Type: types.TypeCanonicalUser,
},
},
},
{
name: "grant header naming the owner",
input: &PutBucketAclInput{
GrantFullControl: &ownerGrantee,
},
want: []Grantee{ownerFullControl},
},
{
name: "create bucket without grants",
input: &PutBucketAclInput{
GrantFullControl: &empty,
GrantRead: &empty,
GrantReadACP: &empty,
GrantWrite: &empty,
GrantWriteACP: &empty,
AccessControlPolicy: &AccessControlPolicy{},
},
want: []Grantee{ownerFullControl},
},
{
name: "canned acl",
input: &PutBucketAclInput{
ACL: types.BucketCannedACLPublicRead,
},
want: []Grantee{
ownerFullControl,
{
Permission: PermissionRead,
Access: "all-users",
Type: types.TypeGroup,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
aclJSON, err := UpdateACL(tt.input, ACL{Owner: "owner"}, existingAccountsIAM{})
assert.NoError(t, err)
acl, err := ParseACL(aclJSON)
assert.NoError(t, err)
assert.Equal(t, "owner", acl.Owner)
assert.Equal(t, tt.want, acl.Grantees)
for _, action := range []Action{GetBucketAclAction, PutBucketAclAction, ListBucketAction, PutObjectAction, PutBucketPolicyAction} {
assert.True(t, aclAllows(acl, "owner", action, false), "owner denied %s", action)
}
})
}
}
+19
View File
@@ -22,6 +22,7 @@ import (
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
@@ -614,6 +615,24 @@ func (c S3ApiController) ListObjects(ctx fiber.Ctx) (*Response, error) {
region = defaultRegion
}
// continuation-token and start-after are ListObjectsV2 arguments, S3
// rejects either one on a V1 listing, with or without a value
queryArgs := ctx.Request().URI().QueryArgs()
if queryArgs.Has("continuation-token") {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, "")
}
if queryArgs.Has("start-after") {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, "")
}
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
+54
View File
@@ -1195,6 +1195,60 @@ func TestS3ApiController_ListObjects(t *testing.T) {
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "start-after is not supported",
input: testInput{
locals: defaultLocals,
beRes: listResult,
queries: map[string]string{
"start-after": "my-key",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, ""),
},
},
{
name: "empty start-after is not supported",
input: testInput{
locals: defaultLocals,
beRes: listResult,
queries: map[string]string{
"start-after": "",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, ""),
},
},
{
name: "continuation-token is not supported",
input: testInput{
locals: defaultLocals,
beRes: listResult,
queries: map[string]string{
"continuation-token": "my-token",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, ""),
},
},
{
name: "invalid max keys",
input: testInput{
+379
View File
@@ -0,0 +1,379 @@
// 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 s3api
import (
"bufio"
"encoding/xml"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/internal/netutil"
"github.com/versity/versitygw/s3api/middlewares"
)
func newTestS3ApiServer(opts ...Option) (*S3ApiServer, error) {
allOpts := append([]Option{WithConcurrencyLimiter(10, 10)}, opts...)
return New(
backend.BackendUnsupported{},
middlewares.RootUserConfig{Access: "access", Secret: "secret"},
"us-east-1",
auth.NewIAMServiceSingle(auth.Account{Access: "access", Secret: "secret"}),
nil,
nil,
nil,
nil,
allOpts...,
)
}
func TestS3ApiServer_Serve(t *testing.T) {
tests := []struct {
name string
sa *S3ApiServer
wantErr bool
port string
}{
{
name: "Serve-invalid-tcp-address",
wantErr: true,
sa: &S3ApiServer{
app: fiber.New(),
backend: backend.BackendUnsupported{},
Router: &S3ApiRouter{},
},
port: "localhost:notaport",
},
{
name: "Serve-invalid-tcp-address-with-certificate",
wantErr: true,
sa: &S3ApiServer{
app: fiber.New(),
backend: backend.BackendUnsupported{},
Router: &S3ApiRouter{},
CertStorage: &netutil.CertStorage{},
},
port: "localhost:notaport",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.sa.ServeMultiPort([]string{tt.port}); (err != nil) != tt.wantErr {
t.Errorf("S3ApiServer.Serve() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
// TestWithOnListenAddrs serves on port 0 and checks that the listen hook
// reports the port the kernel chose, after the bind, for a listener that
// accepts connections.
func TestWithOnListenAddrs(t *testing.T) {
got := make(chan []net.Addr, 1)
sa, err := newTestS3ApiServer(WithOnListenAddrs(func(addrs []net.Addr) { got <- addrs }))
if err != nil {
t.Fatalf("New() error = %v", err)
}
served := make(chan error, 1)
go func() { served <- sa.ServeMultiPort([]string{"127.0.0.1:0"}) }()
var addrs []net.Addr
select {
case addrs = <-got:
case err := <-served:
t.Fatalf("ServeMultiPort() returned before the listen hook fired: %v", err)
case <-time.After(10 * time.Second):
t.Fatal("listen hook did not fire")
}
if len(addrs) != 1 {
t.Fatalf("hook received %d addresses, want 1: %v", len(addrs), addrs)
}
tcp, ok := addrs[0].(*net.TCPAddr)
if !ok || tcp.Port == 0 || !tcp.IP.Equal(net.IPv4(127, 0, 0, 1)) {
t.Fatalf("hook received %v, want a 127.0.0.1 address with the bound port", addrs[0])
}
conn, err := net.DialTimeout("tcp", tcp.String(), 5*time.Second)
if err != nil {
t.Fatalf("dialing the reported address: %v", err)
}
conn.Close()
if err := sa.ShutDown(); err != nil {
t.Fatalf("ShutDown() error = %v", err)
}
// ServeMultiPort reports the closed listener on the way out; only that
// it returns matters here.
select {
case <-served:
case <-time.After(shutDownDuration + time.Second):
t.Fatal("ServeMultiPort() did not return after ShutDown()")
}
}
func TestWithRouteRegistersBeforeMiddleware(t *testing.T) {
const routePath = "/custom/route"
middlewareCalled := false
server, err := newTestS3ApiServer(
WithRoute(http.MethodGet, routePath, func(ctx fiber.Ctx) error {
return ctx.SendStatus(http.StatusNoContent)
}),
WithMiddleware("/", func(ctx fiber.Ctx) error {
middlewareCalled = true
return ctx.SendStatus(http.StatusMisdirectedRequest)
}),
)
if err != nil {
t.Fatalf("New() error = %v", err)
}
resp, err := server.app.Test(httptest.NewRequest(http.MethodGet, routePath, nil))
if err != nil {
t.Fatalf("app.Test() error = %v", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
t.Fatalf("response close error = %v", err)
}
}()
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusNoContent)
}
if middlewareCalled {
t.Fatal("middleware was called for top-level route")
}
}
func TestWithRouteRegistersAfterRateLimiter(t *testing.T) {
const routePath = "/custom/limited"
started := make(chan struct{})
release := make(chan struct{})
firstDone := make(chan error, 1)
var once sync.Once
server, err := newTestS3ApiServer(
WithConcurrencyLimiter(10, 1),
WithRoute(http.MethodGet, routePath, func(ctx fiber.Ctx) error {
once.Do(func() {
close(started)
})
<-release
return ctx.SendStatus(http.StatusNoContent)
}),
)
if err != nil {
t.Fatalf("New() error = %v", err)
}
go func() {
resp, err := server.app.Test(httptest.NewRequest(http.MethodGet, routePath, nil), fiber.TestConfig{Timeout: 0, FailOnTimeout: false})
if err != nil {
firstDone <- err
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
firstDone <- fiber.NewError(resp.StatusCode)
return
}
firstDone <- nil
}()
<-started
resp, err := server.app.Test(httptest.NewRequest(http.MethodGet, routePath, nil), fiber.TestConfig{Timeout: time.Duration(100) * time.Millisecond})
if err != nil {
close(release)
t.Fatalf("second app.Test() error = %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
close(release)
t.Fatalf("second status = %d, want %d", resp.StatusCode, http.StatusServiceUnavailable)
}
close(release)
if err := <-firstDone; err != nil {
t.Fatalf("first request error = %v", err)
}
}
func TestCustomMountValidation(t *testing.T) {
validHandler := func(ctx fiber.Ctx) error {
return ctx.SendStatus(http.StatusNoContent)
}
tests := []struct {
name string
opt Option
wantErr string
}{
{
name: "route empty method",
opt: WithRoute("", "/custom", validHandler),
wantErr: "empty method",
},
{
name: "route unsupported HTTP method",
opt: WithRoute("BREW", "/custom", validHandler),
wantErr: "invalid HTTP method",
},
{
name: "route empty path",
opt: WithRoute(http.MethodGet, "", validHandler),
wantErr: "must start with /",
},
{
name: "route relative path",
opt: WithRoute(http.MethodGet, "custom", validHandler),
wantErr: "must start with /",
},
{
name: "route no handlers",
opt: WithRoute(http.MethodGet, "/custom"),
wantErr: "no handlers",
},
{
name: "route nil handler",
opt: WithRoute(http.MethodGet, "/custom", fiber.Handler(nil)),
wantErr: "nil handler",
},
{
name: "middleware empty prefix",
opt: WithMiddleware("", validHandler),
wantErr: "must start with /",
},
{
name: "middleware relative prefix",
opt: WithMiddleware("custom", validHandler),
wantErr: "must start with /",
},
{
name: "middleware nil handler",
opt: WithMiddleware("/custom", nil),
wantErr: "nil handler",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := newTestS3ApiServer(tt.opt)
if err == nil {
t.Fatal("New() error = nil, want error")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("New() error = %v, want substring %q", err, tt.wantErr)
}
})
}
}
// TestRequestHostHeader sends raw requests to a running server, since Go
// HTTP clients always set a Host header, and checks that an HTTP/1.1
// request without one is rejected with 400 Bad Request, as S3 does.
func TestRequestHostHeader(t *testing.T) {
got := make(chan []net.Addr, 1)
sa, err := newTestS3ApiServer(WithOnListenAddrs(func(addrs []net.Addr) { got <- addrs }))
if err != nil {
t.Fatalf("New() error = %v", err)
}
served := make(chan error, 1)
go func() { served <- sa.ServeMultiPort([]string{"127.0.0.1:0"}) }()
var addr string
select {
case addrs := <-got:
addr = addrs[0].String()
case err := <-served:
t.Fatalf("ServeMultiPort() returned before the listen hook fired: %v", err)
case <-time.After(10 * time.Second):
t.Fatal("listen hook did not fire")
}
t.Cleanup(func() {
if err := sa.ShutDown(); err != nil {
t.Errorf("ShutDown() error = %v", err)
}
<-served
})
tests := []struct {
name string
request string
wantStatus int
wantCode string
}{
{
name: "missing host",
request: "GET / HTTP/1.1\r\n\r\n",
wantStatus: http.StatusBadRequest,
wantCode: "BadRequest",
},
{
name: "empty host",
request: "GET / HTTP/1.1\r\nHost:\r\n\r\n",
wantStatus: http.StatusBadRequest,
wantCode: "BadRequest",
},
{
name: "host present",
request: "GET / HTTP/1.1\r\nHost: " + addr + "\r\n\r\n",
wantStatus: http.StatusForbidden,
wantCode: "AccessDenied",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
t.Fatalf("set deadline: %v", err)
}
if _, err := conn.Write([]byte(tt.request)); err != nil {
t.Fatalf("write request: %v", err)
}
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
if err != nil {
t.Fatalf("read response: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != tt.wantStatus {
t.Fatalf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
}
var apiErr struct{ Code string }
if err := xml.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
t.Fatalf("decode error response: %v", err)
}
if apiErr.Code != tt.wantCode {
t.Fatalf("error code = %q, want %q", apiErr.Code, tt.wantCode)
}
})
}
}
+10
View File
@@ -26,6 +26,8 @@ type InvalidArgErrorCode int
const (
InvalidArgMaxBuckets InvalidArgErrorCode = iota
InvalidArgNegativeMaxKeys
InvalidArgStartAfter
InvalidArgContinuationToken
InvalidArgObjectAttributes
InvalidArgPartNumber
InvalidArgCompleteMpPartNumber
@@ -90,6 +92,14 @@ var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{
Description: "max-keys cannot be negative",
ArgumentName: "maxKeys",
},
InvalidArgStartAfter: {
Description: "startAfter only supported in REST.GET.BUCKET with list-type=2",
ArgumentName: "start-after",
},
InvalidArgContinuationToken: {
Description: "continuation-token only supported in REST.GET.BUCKET with list-type=2",
ArgumentName: "continuation-token",
},
InvalidArgObjectAttributes: {
Description: "Invalid attribute name specified.",
ArgumentName: "x-amz-object-attributes",
+656
View File
@@ -0,0 +1,656 @@
// 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 integration
import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
func CreateBucket_invalid_bucket_name(s *S3Conf) error {
testName := "CreateBucket_invalid_bucket_name"
runF(testName)
err := setup(s, "aa")
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
err = setup(s, ".gitignore")
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
err = setup(s, "my-bucket.")
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
err = setup(s, "bucket-%")
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func CreateBucket_as_user(s *S3Conf) error {
testName := "CreateBucket_as_user"
runF(testName)
testuser := getUser("user")
cfg := *s
cfg.awsID = testuser.access
cfg.awsSecret = testuser.secret
err := createUsers(s, []user{testuser})
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
err = setup(&cfg, getBucketName())
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func CreateBucket_existing_bucket(s *S3Conf) error {
testName := "CreateBucket_existing_bucket"
runF(testName)
bucket := getBucketName()
adminUser := getUser("admin")
if err := createUsers(s, []user{adminUser}); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
adminCfg := *s
adminCfg.awsID = adminUser.access
adminCfg.awsSecret = adminUser.secret
err := setup(&adminCfg, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
err = setup(s, bucket)
var bne *types.BucketAlreadyExists
if !errors.As(err, &bne) {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
err = teardown(s, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func CreateBucket_owned_by_you(s *S3Conf) error {
testName := "CreateBucket_owned_by_you"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
})
cancel()
var bErr *types.BucketAlreadyOwnedByYou
if !errors.As(err, &bErr) {
return fmt.Errorf("expected error to be %w, instead got %w", s3err.GetAPIError(s3err.ErrBucketAlreadyOwnedByYou), err)
}
return nil
})
}
func CreateBucket_invalid_ownership(s *S3Conf) error {
testName := "CreateBucket_invalid_ownership"
runF(testName)
invalidOwnership := types.ObjectOwnership("invalid_ownership")
err := setup(s, getBucketName(), withOwnership(invalidOwnership))
if err := checkApiErr(err, s3err.GetInvalidArgObjectOwnership(string(invalidOwnership))); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func CreateBucket_ownership_with_acl(s *S3Conf) error {
testName := "CreateBucket_ownership_with_acl"
runF(testName)
client := s.GetClient()
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: getPtr(getBucketName()),
ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced,
ACL: types.BucketCannedACLPublicRead,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketAclWithObjectOwnership)); err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func CreateBucket_default_acl(s *S3Conf) error {
testName := "CreateBucket_default_acl"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &bucket})
cancel()
if err != nil {
return err
}
if getString(out.Owner.ID) != s.awsID {
return fmt.Errorf("expected bucket owner to be %v, instead got %v",
s.awsID, getString(out.Owner.ID))
}
if len(out.Grants) != 1 {
return fmt.Errorf("expected grants length to be 1, instead got %v",
len(out.Grants))
}
grt := out.Grants[0]
if grt.Permission != types.PermissionFullControl {
return fmt.Errorf("expected the grantee to have full-control permission, instead got %v",
grt.Permission)
}
if getString(grt.Grantee.ID) != s.awsID {
return fmt.Errorf("expected the grantee id to be %v, instead got %v",
s.awsID, getString(grt.Grantee.ID))
}
return nil
})
}
func CreateBucket_non_default_acl(s *S3Conf) error {
testName := "CreateBucket_non_default_acl"
runF(testName)
testuser1, testuser2, testuser3 := getUser("user"), getUser("user"), getUser("user")
err := createUsers(s, []user{testuser1, testuser2, testuser3})
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
grants := []types.Grant{
{
Grantee: &types.Grantee{
ID: &testuser1.access,
Type: types.TypeCanonicalUser,
},
Permission: types.PermissionFullControl,
},
{
Grantee: &types.Grantee{
ID: &testuser2.access,
Type: types.TypeCanonicalUser,
},
Permission: types.PermissionReadAcp,
},
{
Grantee: &types.Grantee{
ID: &testuser3.access,
Type: types.TypeCanonicalUser,
},
Permission: types.PermissionWrite,
},
}
bucket := getBucketName()
client := s.GetClient()
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
GrantFullControl: &testuser1.access,
GrantReadACP: &testuser2.access,
GrantWrite: &testuser3.access,
ObjectOwnership: types.ObjectOwnershipBucketOwnerPreferred,
})
cancel()
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &bucket})
cancel()
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
if !compareGrants(out.Grants, grants) {
failF("%v: expected bucket acl grants to be %v, instead got %v", testName, grants, out.Grants)
return fmt.Errorf("%v: expected bucket acl grants to be %v, instead got %v", testName, grants, out.Grants)
}
err = teardown(s, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func CreateBucket_private_canned_acl(s *S3Conf) error {
testName := "CreateBucket_private_canned_acl"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
ACL: types.BucketCannedACLPrivate,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if getString(res.Owner.ID) != s.awsID {
return fmt.Errorf("expected bucket owner to be %v, instead got %v",
s.awsID, getString(res.Owner.ID))
}
if len(res.Grants) != 1 {
return fmt.Errorf("expected grants length to be 1, instead got %v",
len(res.Grants))
}
grt := res.Grants[0]
if grt.Permission != types.PermissionFullControl {
return fmt.Errorf("expected the grantee to have full-control permission, instead got %v",
grt.Permission)
}
if getString(grt.Grantee.ID) != s.awsID {
return fmt.Errorf("expected the grantee id to be %v, instead got %v",
s.awsID, getString(grt.Grantee.ID))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if out.OwnershipControls == nil {
return fmt.Errorf("unexpected nil OwnershipControls")
}
if len(out.OwnershipControls.Rules) != 1 {
return fmt.Errorf("expected the OwnershipControls rules length to be 1, instead got %v", len(out.OwnershipControls.Rules))
}
if out.OwnershipControls.Rules[0].ObjectOwnership != types.ObjectOwnershipBucketOwnerPreferred {
return fmt.Errorf("expected the ObjectOwnership to be %s, instead got %s", types.ObjectOwnershipBucketOwnerPreferred, out.OwnershipControls.Rules[0].ObjectOwnership)
}
return teardown(s, bucket)
})
}
func CreateBucket_private_canned_acl_bucket_owner_enforced_ownership(s *S3Conf) error {
testName := "CreateBucket_private_canned_acl_bucket_owner_enforced_ownership"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
ACL: types.BucketCannedACLPrivate,
ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if getString(res.Owner.ID) != s.awsID {
return fmt.Errorf("expected bucket owner to be %v, instead got %v",
s.awsID, getString(res.Owner.ID))
}
if len(res.Grants) != 1 {
return fmt.Errorf("expected grants length to be 1, instead got %v",
len(res.Grants))
}
grt := res.Grants[0]
if grt.Permission != types.PermissionFullControl {
return fmt.Errorf("expected the grantee to have full-control permission, instead got %v",
grt.Permission)
}
if getString(grt.Grantee.ID) != s.awsID {
return fmt.Errorf("expected the grantee id to be %v, instead got %v",
s.awsID, getString(grt.Grantee.ID))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if out.OwnershipControls == nil {
return fmt.Errorf("unexpected nil OwnershipControls")
}
if len(out.OwnershipControls.Rules) != 1 {
return fmt.Errorf("expected the OwnershipControls rules length to be 1, instead got %v", len(out.OwnershipControls.Rules))
}
if out.OwnershipControls.Rules[0].ObjectOwnership != types.ObjectOwnershipBucketOwnerEnforced {
return fmt.Errorf("expected the ObjectOwnership to be %s, instead got %s", types.ObjectOwnershipBucketOwnerEnforced, out.OwnershipControls.Rules[0].ObjectOwnership)
}
return teardown(s, bucket)
})
}
func CreateBucket_default_object_lock(s *S3Conf) error {
testName := "CreateBucket_default_object_lock"
runF(testName)
bucket := getBucketName()
lockEnabled := true
client := s.GetClient()
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
ObjectLockEnabledForBucket: &lockEnabled,
})
cancel()
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{
Bucket: &bucket,
})
cancel()
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
if resp.ObjectLockConfiguration.ObjectLockEnabled != types.ObjectLockEnabledEnabled {
failF("%v: expected object lock to be enabled", testName)
return fmt.Errorf("%v: expected object lock to be enabled", testName)
}
err = teardown(s, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func CreateBucket_invalid_location_constraint(s *S3Conf) error {
testName := "CreateBucket_invalid_location_constraint"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
var region types.BucketLocationConstraint
if types.BucketLocationConstraint(s.awsID) == types.BucketLocationConstraintUsWest1 {
region = types.BucketLocationConstraintUsWest2
} else {
region = types.BucketLocationConstraintUsWest1
}
createBucket := func(region types.BucketLocationConstraint) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
CreateBucketConfiguration: &types.CreateBucketConfiguration{
LocationConstraint: region,
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidLocationConstraint))
}
for _, lConstraint := range []types.BucketLocationConstraint{region, "us-east-1"} {
err := createBucket(lConstraint)
if err != nil {
return err
}
}
return nil
})
}
func CreateBucket_long_tags(s *S3Conf) error {
testName := "CreateBucket_long_tags"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
tagging := []types.Tag{{Key: getPtr(genRandString(200)), Value: getPtr("val")}}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
CreateBucketConfiguration: &types.CreateBucketConfiguration{
Tags: tagging,
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidTagKey)); err != nil {
return err
}
tagging = []types.Tag{{Key: getPtr("key"), Value: getPtr(genRandString(300))}}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
CreateBucketConfiguration: &types.CreateBucketConfiguration{
Tags: tagging,
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidTagValue)); err != nil {
return err
}
return nil
})
}
func CreateBucket_invalid_tags(s *S3Conf) error {
testName := "CreateBucket_invalid_tags"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
for i, test := range []struct {
tags []types.Tag
err s3err.APIError
}{
// invalid tag key tests
{[]types.Tag{{Key: getPtr("user!name"), Value: getPtr("value")}}, s3err.GetAPIError(s3err.ErrInvalidTagKey)},
{[]types.Tag{{Key: getPtr("foo#bar"), Value: getPtr("value")}}, s3err.GetAPIError(s3err.ErrInvalidTagKey)},
{[]types.Tag{
{Key: getPtr("validkey"), Value: getPtr("validvalue")},
{Key: getPtr("data%20"), Value: getPtr("value")},
}, s3err.GetAPIError(s3err.ErrInvalidTagKey)},
{[]types.Tag{
{Key: getPtr("abcd"), Value: getPtr("xyz123")},
{Key: getPtr("a*b"), Value: getPtr("value")},
}, s3err.GetAPIError(s3err.ErrInvalidTagKey)},
// invalid tag value tests
{[]types.Tag{
{Key: getPtr("hello"), Value: getPtr("world")},
{Key: getPtr("key"), Value: getPtr("name?test")},
}, s3err.GetAPIError(s3err.ErrInvalidTagValue)},
{[]types.Tag{
{Key: getPtr("foo"), Value: getPtr("bar")},
{Key: getPtr("key"), Value: getPtr("`path")},
}, s3err.GetAPIError(s3err.ErrInvalidTagValue)},
{[]types.Tag{{Key: getPtr("valid"), Value: getPtr("comma,separated")}}, s3err.GetAPIError(s3err.ErrInvalidTagValue)},
{[]types.Tag{{Key: getPtr("valid"), Value: getPtr("semicolon;test")}}, s3err.GetAPIError(s3err.ErrInvalidTagValue)},
{[]types.Tag{{Key: getPtr("valid"), Value: getPtr("(parentheses)")}}, s3err.GetAPIError(s3err.ErrInvalidTagValue)},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
CreateBucketConfiguration: &types.CreateBucketConfiguration{
Tags: test.tags,
},
})
cancel()
if err == nil {
return fmt.Errorf("test %v failed: expected err %w, instead got nil", i+1, test.err)
}
if err := checkApiErr(err, test.err); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
return nil
})
}
func CreateBucket_duplicate_keys(s *S3Conf) error {
testName := "CreateBucket_duplicate_keys"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
tagging := []types.Tag{
{Key: getPtr("key"), Value: getPtr("value")},
{Key: getPtr("key"), Value: getPtr("value-1")},
{Key: getPtr("key-1"), Value: getPtr("value-2")},
{Key: getPtr("key-2"), Value: getPtr("value-3")},
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
CreateBucketConfiguration: &types.CreateBucketConfiguration{
Tags: tagging,
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrDuplicateTagKey)); err != nil {
return err
}
return nil
})
}
func CreateBucket_tag_count_limit(s *S3Conf) error {
testName := "CreateBucket_tag_count_limit"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
tagSet := []types.Tag{}
for i := range 51 {
tagSet = append(tagSet, types.Tag{
Key: getPtr(fmt.Sprintf("key-%v", i)),
Value: getPtr(genRandString(10)),
})
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
CreateBucketConfiguration: &types.CreateBucketConfiguration{
Tags: tagSet,
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrBucketTaggingLimited))
})
}
func CreateBucket_invalid_canned_acl(s *S3Conf) error {
testName := "CreateBucket_invalid_canned_acl"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
ACL: types.BucketCannedACL("invalid_acl"),
})
cancel()
return checkSdkApiErr(err, "InvalidArgument")
})
}
func CreateBucket_success(s *S3Conf) error {
testName := "CreateBucket_success"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
expectedLocation := "/" + bucket
if getString(res.Location) != expectedLocation {
return fmt.Errorf("expected the bucket Location to be %s, instead got %s", expectedLocation, getString(res.Location))
}
expectedArn := "arn:aws:s3:::" + bucket
if getString(res.BucketArn) != expectedArn {
return fmt.Errorf("expected the bucket arn to be %s, instead got %s", expectedArn, getString(res.BucketArn))
}
return teardown(s, bucket)
})
}
+46
View File
@@ -17,6 +17,8 @@ package integration
import (
"context"
"fmt"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
@@ -193,6 +195,50 @@ func ListObjects_invalid_max_keys(s *S3Conf) error {
})
}
// start-after and continuation-token belong to ListObjectsV2, so S3 rejects
// either one on a V1 listing, with or without a value. The SDK ListObjects
// input has neither field, so the requests are signed and sent by hand.
func ListObjects_start_after(s *S3Conf) error {
testName := "ListObjects_start_after"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
_, err := putObjects(s3client, []string{"bar", "baz", "foo"}, bucket)
if err != nil {
return err
}
startAfterErr := s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, "")
contTokenErr := s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, "")
for _, test := range []struct {
query string
expected s3err.S3Error
}{
{"start-after=bar", startAfterErr},
{"start-after=", startAfterErr},
{"start-after", startAfterErr},
{"continuation-token=bar", contTokenErr},
{"continuation-token=", contTokenErr},
} {
req, err := createSignedReq(http.MethodGet, s.endpoint,
fmt.Sprintf("%v?%v", bucket, test.query),
s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), nil)
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
if err := checkHTTPResponseApiErr(resp, test.expected); err != nil {
return fmt.Errorf("%v: %w", test.query, err)
}
}
return nil
})
}
func ListObjects_max_keys_0(s *S3Conf) error {
testName := "ListObjects_max_keys_0"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+2
View File
@@ -312,6 +312,7 @@ func TestListObjects(ts *TestState) {
ts.Run(ListObjects_non_existing_bucket)
ts.Run(ListObjects_with_prefix)
ts.Run(ListObjects_truncated)
ts.Run(ListObjects_truncated_with_delimiter)
ts.Run(ListObjects_paginated)
ts.Run(ListObjects_invalid_max_keys)
ts.Run(ListObjects_max_keys_0)
@@ -3151,6 +3152,7 @@ func GetIntTests() IntTests {
"ListObjects_non_existing_bucket": ListObjects_non_existing_bucket,
"ListObjects_with_prefix": ListObjects_with_prefix,
"ListObjects_truncated": ListObjects_truncated,
"ListObjects_truncated_with_delimiter": ListObjects_truncated_with_delimiter,
"ListObjects_paginated": ListObjects_paginated,
"ListObjects_invalid_max_keys": ListObjects_invalid_max_keys,
"ListObjects_max_keys_0": ListObjects_max_keys_0,
+350
View File
@@ -0,0 +1,350 @@
// 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 integration
import (
"context"
"encoding/xml"
"fmt"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
func IAM_user_access_denied(s *S3Conf) error {
testName := "IAM_user_access_denied"
runF(testName)
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
out, err := execCommand(s.getAdminCommand("-a", testuser.access, "-s", testuser.secret, "-er", s.endpoint, "delete-user", "-a", "random_access")...)
if err == nil {
failF("%v: expected cmd error", testName)
return fmt.Errorf("%v: expected cmd error", testName)
}
if !strings.Contains(string(out), s3err.GetAPIError(s3err.ErrAdminAccessDenied).Code) {
failF("%v: expected response error message to be %v, instead got %s",
testName, s3err.GetAPIError(s3err.ErrAdminAccessDenied).Error(), out)
return fmt.Errorf("%v: expected response error message to be %v, instead got %s",
testName, s3err.GetAPIError(s3err.ErrAdminAccessDenied).Error(), out)
}
passF(testName)
return nil
}
func IAM_userplus_access_denied(s *S3Conf) error {
testName := "IAM_userplus_access_denied"
runF(testName)
testuser := getUser("userplus")
err := createUsers(s, []user{testuser})
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
out, err := execCommand(s.getAdminCommand("-a", testuser.access, "-s", testuser.secret, "-er", s.endpoint, "delete-user", "-a", "random_access")...)
if err == nil {
failF("%v: expected cmd error", testName)
return fmt.Errorf("%v: expected cmd error", testName)
}
if !strings.Contains(string(out), s3err.GetAPIError(s3err.ErrAdminAccessDenied).Code) {
failF("%v: expected response error message to be %v, instead got %s",
testName, s3err.GetAPIError(s3err.ErrAdminAccessDenied).Error(), out)
return fmt.Errorf("%v: expected response error message to be %v, instead got %s",
testName, s3err.GetAPIError(s3err.ErrAdminAccessDenied).Error(), out)
}
passF(testName)
return nil
}
func IAM_userplus_CreateBucket(s *S3Conf) error {
testName := "IAM_userplus_CreateBucket"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("userplus")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
cfg := *s
cfg.awsID = testuser.access
cfg.awsSecret = testuser.secret
bckt := getBucketName()
err = setup(&cfg, bckt)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bckt})
cancel()
if err != nil {
return err
}
err = teardown(&cfg, bckt)
if err != nil {
return err
}
return nil
})
}
func IAM_admin_ChangeBucketOwner(s *S3Conf) error {
testName := "IAM_admin_ChangeBucketOwner"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser, adminuser := getUser("user"), getUser("admin")
err := createUsers(s, []user{adminuser, testuser})
if err != nil {
return err
}
err = changeBucketsOwner(s, []string{bucket}, testuser.access)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &bucket})
cancel()
if err != nil {
return err
}
if getString(resp.Owner.ID) != testuser.access {
return fmt.Errorf("expected the bucket owner to be %v, instead got %v",
testuser.access, getString(resp.Owner.ID))
}
return nil
})
}
func IAM_ChangeBucketOwner_back_to_root(s *S3Conf) error {
testName := "IAM_ChangeBucketOwner_back_to_root"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
// Change the bucket ownership to a random user
if err := changeBucketsOwner(s, []string{bucket}, testuser.access); err != nil {
return err
}
// Change the bucket ownership back to the root user
if err := changeBucketsOwner(s, []string{bucket}, s.awsID); err != nil {
return err
}
return nil
})
}
func IAM_ListBuckets(s *S3Conf) error {
testName := "IAM_ListBuckets"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return listBuckets(s)
})
}
func IAM_CreateBucket_empty_owner_header(s *S3Conf) error {
testName := "IAM_CreateBucket_empty_owner_header"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
req, err := createSignedReq(
http.MethodPatch,
s.endpoint,
fmt.Sprintf("%s/create", bucket),
s.awsID,
s.awsSecret,
"s3",
s.awsRegion,
"",
nil,
time.Now(),
nil,
)
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrAdminEmptyBucketOwnerHeader))
})
}
func IAM_CreateBucket_non_existing_user(s *S3Conf) error {
testName := "IAM_CreateBucket_non_existing_user"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
req, err := createSignedReq(
http.MethodPatch,
s.endpoint,
fmt.Sprintf("%s/create", bucket),
s.awsID,
s.awsSecret,
"s3",
s.awsRegion,
"",
nil,
time.Now(),
map[string]string{
"x-vgw-owner": "non-existing-user",
},
)
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrAdminUserNotFound))
})
}
func IAM_CreateBucket_success(s *S3Conf) error {
testName := "IAM_CreateBucket_success"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
tagSet := []types.Tag{
{Key: getPtr("key"), Value: getPtr("value")},
}
body, err := xml.Marshal(s3response.CreateBucketConfiguration{
TagSet: tagSet,
})
if err != nil {
return err
}
testUser1, testUser2 := getUser("user"), getUser("user")
err = createUsers(s, []user{testUser1, testUser2})
if err != nil {
return err
}
req, err := createSignedReq(
http.MethodPatch,
s.endpoint,
fmt.Sprintf("%s/create", bucket),
s.awsID,
s.awsSecret,
"s3",
s.awsRegion,
"",
body,
time.Now(),
map[string]string{
"x-amz-bucket-object-lock-enabled": "true",
"x-amz-object-ownership": string(types.ObjectOwnershipBucketOwnerPreferred),
"x-amz-grant-read": testUser2.access,
"x-vgw-owner": testUser1.access,
},
)
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("expected the response status code to be %v, instead got %v", http.StatusCreated, resp.StatusCode)
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
tagging, err := s3client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if !areTagsSame(tagSet, tagging.TagSet) {
return fmt.Errorf("expected the bucket tagging to be %v, instead got %v", tagSet, tagging.TagSet)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
ownership, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
var ownershipControls types.ObjectOwnership
if ownership.OwnershipControls != nil && len(ownership.OwnershipControls.Rules) == 1 {
ownershipControls = ownership.OwnershipControls.Rules[0].ObjectOwnership
}
if ownershipControls != types.ObjectOwnershipBucketOwnerPreferred {
return fmt.Errorf("expected the bucket ownership controls to be %s, instaed got %s", types.ObjectOwnershipBucketOwnerPreferred, ownershipControls)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
acl, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if getString(acl.Owner.ID) != testUser1.access {
return fmt.Errorf("expected the bucket owner to be %v, instead got %v", testUser1.access, getString(acl.Owner.ID))
}
// the grant header replaces the acl, so the owner gets no FULL_CONTROL grant
if len(acl.Grants) != 1 {
return fmt.Errorf("expected the length of acl grants to be 1, instead got %v", len(acl.Grants))
}
grant := acl.Grants[0]
if getString(grant.Grantee.ID) != testUser2.access {
return fmt.Errorf("expected the grantee ID to be %v, instaed got %v", testUser2.access, getString(grant.Grantee.ID))
}
if grant.Permission != types.PermissionRead {
return fmt.Errorf("expected the %v user permission to be %s, instead got %s", testUser2.access, types.PermissionRead, grant.Permission)
}
return teardown(s, bucket)
})
}
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env bats
# Copyright 2024 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.
load ./bats-support/load
load ./bats-assert/load
source ./tests/commands/put_object.sh
source ./tests/drivers/file.sh
source ./tests/drivers/create_bucket/create_bucket_rest.sh
source ./tests/drivers/get_bucket_acl/get_bucket_acl_rest.sh
source ./tests/logger.sh
source ./tests/setup.sh
source ./tests/util/util_object.sh
source ./tests/util/util_public_access_block.sh
export RUN_USERS=true
if [ "$SKIP_ACL_TESTING" == "true" ] || [ "$SKIP_USERS_TESTS" == "true" ]; then
skip "Skipping ACL tests"
exit 0
fi
# tags: curl,minimal-request,GetBucketAcl
@test "REST - get ACL" {
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run setup_bucket_v2 "$bucket_name"
assert_success
run get_and_check_acl_rest "$bucket_name"
assert_success
}
# tags: curl,minimal-request,PutBucketAcl
@test "REST - put ACL" {
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
test_file="test_file"
run setup_bucket_and_file_v2 "$bucket_name" "$test_file"
assert_success
run put_bucket_ownership_controls "$bucket_name" "BucketOwnerPreferred"
assert_success
run put_object "s3api" "$TEST_FILE_FOLDER/$test_file" "$bucket_name" "$test_file"
assert_success
run create_versitygw_acl_user_or_get_direct_user "$USERNAME_ONE" "$PASSWORD_ONE"
assert_success
canonical_id=${lines[0]}
user_canonical_id=${lines[1]}
username=${lines[2]}
password=${lines[3]}
run setup_acl "$TEST_FILE_FOLDER/acl-file.txt" "CanonicalUser" "$user_canonical_id" "READ" "$canonical_id"
assert_success
run list_objects_with_user_rest_verify_access_denied "$bucket_name" "$username" "$password"
assert_success
run put_bucket_acl_rest "$bucket_name" "$TEST_FILE_FOLDER/acl-file.txt"
assert_success
if [ "$DIRECT" == "true" ]; then
sleep 5
fi
run list_objects_with_user_rest_verify_success "$bucket_name" "$username" "$password" "$test_file"
assert_success
}
# tags: curl,PutBucketAcl,x-amz-acl
@test "REST - put public-read canned acl" {
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
test_file="test_file"
run setup_bucket_and_file_v2 "$bucket_name" "$test_file"
assert_success
run put_bucket_ownership_controls "$bucket_name" "BucketOwnerPreferred"
assert_success
run put_object "s3api" "$TEST_FILE_FOLDER/$test_file" "$bucket_name" "$test_file"
assert_success
run create_versitygw_acl_user_or_get_direct_user "$USERNAME_ONE" "$PASSWORD_ONE"
assert_success
canonical_id=${lines[0]}
user_canonical_id=${lines[1]}
username=${lines[2]}
password=${lines[3]}
run list_objects_with_user_rest_verify_access_denied "$bucket_name" "$username" "$password"
assert_success
if [ "$DIRECT" == "true" ]; then
run allow_public_access "$bucket_name"
assert_success
fi
run put_canned_acl_rest "$bucket_name" "public-read"
assert_success
run list_objects_with_user_rest_verify_success "$bucket_name" "$username" "$password" "$test_file"
assert_success
}
# tags: curl,minimal-request,PutBucketAcl
@test "REST - put invalid ACL" {
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run setup_bucket_v2 "$bucket_name"
assert_success
run put_bucket_ownership_controls "$bucket_name" "BucketOwnerPreferred"
assert_success
run create_versitygw_acl_user_or_get_direct_user "$USERNAME_ONE" "$PASSWORD_ONE"
assert_success
canonical_id=${lines[0]}
user_canonical_id=${lines[1]}
username=${lines[2]}
password=${lines[3]}
run setup_acl "$TEST_FILE_FOLDER/acl-file.txt" "CanonicalUser" "$user_canonical_id" "READD" "$canonical_id"
assert_success
if [ "$DIRECT" == "true" ]; then
run allow_public_access "$bucket_name"
assert_success
fi
run put_invalid_acl_rest_verify_failure "$bucket_name" "$TEST_FILE_FOLDER/acl-file.txt"
assert_success
}
# tags: curl,PutBucketAcl,x-amz-acl
@test "REST - put public-read-write canned acl" {
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
test_file="test_file"
run setup_bucket_and_file_v2 "$bucket_name" "$test_file"
assert_success
run put_bucket_ownership_controls "$bucket_name" "BucketOwnerPreferred"
assert_success
run create_versitygw_acl_user_or_get_direct_user "$USERNAME_ONE" "$PASSWORD_ONE"
assert_success
canonical_id=${lines[0]}
user_canonical_id=${lines[1]}
username=${lines[2]}
password=${lines[3]}
run put_object_with_user "s3api" "$TEST_FILE_FOLDER/$test_file" "$bucket_name" "$test_file" "$username" "$password"
assert_failure
if [ "$DIRECT" == "true" ]; then
run allow_public_access "$bucket_name"
assert_success
fi
run put_canned_acl_rest "$bucket_name" "public-read-write"
assert_success
run put_object_with_user "s3api" "$TEST_FILE_FOLDER/$test_file" "$bucket_name" "$test_file" "$username" "$password"
assert_success
}
# tags: curl,PutBucketAcl,x-amz-acl
@test "REST - invalid canned acl" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/1367"
fi
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
test_file="test_file"
run setup_bucket_and_file_v2 "$bucket_name" "$test_file"
assert_success
run put_bucket_ownership_controls "$bucket_name" "BucketOwnerPreferred"
assert_success
run put_bucket_acl_rest_canned_invalid "$bucket_name" "privatee"
assert_success
}
# tags: curl,GetBucketAcl,x-amz-grant-read-acp
@test "REST - FULL_CONTROL permission not returned for owner after CreateBucket with GRANT_READ_ACP" {
if [ "$RECREATE_BUCKETS" == "false" ]; then
skip "skip bucket create tests for static buckets"
fi
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run create_versitygw_acl_user_or_get_direct_user "$USERNAME_ONE" "$PASSWORD_ONE"
assert_success
canonical_id=${lines[0]}
user_canonical_id=${lines[1]}
username=${lines[2]}
password=${lines[3]}
run bucket_cleanup_if_bucket_exists "$bucket_name"
assert_success
if [ "$DIRECT" == "true" ]; then
id="id=$user_canonical_id"
else
id="$user_canonical_id"
fi
envs="GRANT_READ_ACP=$id OBJECT_OWNERSHIP=BucketOwnerPreferred"
run create_bucket_rest_expect_success "$bucket_name" "$envs"
assert_success
run get_bucket_acl_rest "$bucket_name" "" "check_that_acl_xml_does_not_have_owner_permission"
assert_success
}
+8 -27
View File
@@ -105,9 +105,6 @@ export RUN_USERS=true
# tags: openssl,ListBuckets,required-headers,host,invalid-header
@test "REST - missing host parameter" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/1530"
fi
run send_openssl_go_command "400" "-missingHostParam"
assert_success
}
@@ -220,20 +217,12 @@ export RUN_USERS=true
# tags: curl,invalid-method
@test "REST - service route - invalid POST route" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/1810"
fi
run get_file_name
assert_success
file_name=$output
local bucket_name
run get_bucket_name "$BUCKET_ONE_NAME"
run setup_bucket_v3 "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run setup_bucket_and_add_file "$bucket_name" "$file_name"
assert_success
run send_rest_go_command_expect_error_with_specific_arg_names_values "405" "MethodNotAllowed" "is not allowed" 4 "Method" "POST" "ResourceType" "SERVICE" "-method" "POST"
assert_success
}
@@ -349,32 +338,24 @@ export RUN_USERS=true
# tags: curl,ListBuckets,required-headers,x-amz-date
@test "REST - ListBuckets - omit date" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/1934"
fi
run get_bucket_name "$BUCKET_ONE_NAME"
local bucket_name
run setup_bucket_v3 "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run setup_bucket "$bucket_name"
assert_success
run send_rest_go_command_expect_error "403" "AccessDenied" "AWS authentication requires a valid Date or x-amz-date header" "-omitDate"
assert_success
}
# tags: curl,ListBuckets,required-headers,x-amz-date
@test "REST - ListBuckets - invalid date" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/1934"
fi
run get_bucket_name "$BUCKET_ONE_NAME"
local bucket_name
run setup_bucket_v3 "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run setup_bucket "$bucket_name"
assert_success
run send_rest_go_command_expect_error "403" "AccessDenied" "AWS authentication requires a valid Date or x-amz-date header" "-customDate" "ABCDEFG"
assert_success
}
-3
View File
@@ -122,9 +122,6 @@ source ./tests/drivers/objects_and_versions.sh
# tags: curl,ListObjects,minimal-request
@test "REST - list objects v1 - no NextMarker without delimiter" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/999"
fi
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"