Compare commits
10
Commits
485130eb9f
...
158d7649bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
158d7649bd | ||
|
|
0e3045b87f | ||
|
|
3fee7f49c9 | ||
|
|
deab0fc412 | ||
|
|
cf6c64c93a | ||
|
|
276128b42b | ||
|
|
613c71290f | ||
|
|
7dce327c11 | ||
|
|
295bb05dd1 | ||
|
|
b8628c396f |
No files matched your search
@@ -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
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/versity/versitygw/auth"
|
"github.com/versity/versitygw/auth"
|
||||||
"github.com/versity/versitygw/s3api/utils"
|
"github.com/versity/versitygw/s3api/utils"
|
||||||
|
"github.com/versity/versitygw/s3err"
|
||||||
"github.com/versity/versitygw/s3response"
|
"github.com/versity/versitygw/s3response"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -614,6 +615,24 @@ func (c S3ApiController) ListObjects(ctx fiber.Ctx) (*Response, error) {
|
|||||||
region = defaultRegion
|
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{
|
err := c.verifyAccess(ctx, auth.AccessOptions{
|
||||||
Acl: parsedAcl,
|
Acl: parsedAcl,
|
||||||
AclPermission: auth.PermissionRead,
|
AclPermission: auth.PermissionRead,
|
||||||
|
|||||||
@@ -1195,6 +1195,60 @@ func TestS3ApiController_ListObjects(t *testing.T) {
|
|||||||
err: s3err.GetAPIError(s3err.ErrAccessDenied),
|
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",
|
name: "invalid max keys",
|
||||||
input: testInput{
|
input: testInput{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ type InvalidArgErrorCode int
|
|||||||
const (
|
const (
|
||||||
InvalidArgMaxBuckets InvalidArgErrorCode = iota
|
InvalidArgMaxBuckets InvalidArgErrorCode = iota
|
||||||
InvalidArgNegativeMaxKeys
|
InvalidArgNegativeMaxKeys
|
||||||
|
InvalidArgStartAfter
|
||||||
|
InvalidArgContinuationToken
|
||||||
InvalidArgObjectAttributes
|
InvalidArgObjectAttributes
|
||||||
InvalidArgPartNumber
|
InvalidArgPartNumber
|
||||||
InvalidArgCompleteMpPartNumber
|
InvalidArgCompleteMpPartNumber
|
||||||
@@ -90,6 +92,14 @@ var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{
|
|||||||
Description: "max-keys cannot be negative",
|
Description: "max-keys cannot be negative",
|
||||||
ArgumentName: "maxKeys",
|
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: {
|
InvalidArgObjectAttributes: {
|
||||||
Description: "Invalid attribute name specified.",
|
Description: "Invalid attribute name specified.",
|
||||||
ArgumentName: "x-amz-object-attributes",
|
ArgumentName: "x-amz-object-attributes",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
"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 {
|
func ListObjects_max_keys_0(s *S3Conf) error {
|
||||||
testName := "ListObjects_max_keys_0"
|
testName := "ListObjects_max_keys_0"
|
||||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ func TestListObjects(ts *TestState) {
|
|||||||
ts.Run(ListObjects_non_existing_bucket)
|
ts.Run(ListObjects_non_existing_bucket)
|
||||||
ts.Run(ListObjects_with_prefix)
|
ts.Run(ListObjects_with_prefix)
|
||||||
ts.Run(ListObjects_truncated)
|
ts.Run(ListObjects_truncated)
|
||||||
|
ts.Run(ListObjects_truncated_with_delimiter)
|
||||||
ts.Run(ListObjects_paginated)
|
ts.Run(ListObjects_paginated)
|
||||||
ts.Run(ListObjects_invalid_max_keys)
|
ts.Run(ListObjects_invalid_max_keys)
|
||||||
ts.Run(ListObjects_max_keys_0)
|
ts.Run(ListObjects_max_keys_0)
|
||||||
@@ -3151,6 +3152,7 @@ func GetIntTests() IntTests {
|
|||||||
"ListObjects_non_existing_bucket": ListObjects_non_existing_bucket,
|
"ListObjects_non_existing_bucket": ListObjects_non_existing_bucket,
|
||||||
"ListObjects_with_prefix": ListObjects_with_prefix,
|
"ListObjects_with_prefix": ListObjects_with_prefix,
|
||||||
"ListObjects_truncated": ListObjects_truncated,
|
"ListObjects_truncated": ListObjects_truncated,
|
||||||
|
"ListObjects_truncated_with_delimiter": ListObjects_truncated_with_delimiter,
|
||||||
"ListObjects_paginated": ListObjects_paginated,
|
"ListObjects_paginated": ListObjects_paginated,
|
||||||
"ListObjects_invalid_max_keys": ListObjects_invalid_max_keys,
|
"ListObjects_invalid_max_keys": ListObjects_invalid_max_keys,
|
||||||
"ListObjects_max_keys_0": ListObjects_max_keys_0,
|
"ListObjects_max_keys_0": ListObjects_max_keys_0,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -105,9 +105,6 @@ export RUN_USERS=true
|
|||||||
|
|
||||||
# tags: openssl,ListBuckets,required-headers,host,invalid-header
|
# tags: openssl,ListBuckets,required-headers,host,invalid-header
|
||||||
@test "REST - missing host parameter" {
|
@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"
|
run send_openssl_go_command "400" "-missingHostParam"
|
||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
@@ -220,20 +217,12 @@ export RUN_USERS=true
|
|||||||
|
|
||||||
# tags: curl,invalid-method
|
# tags: curl,invalid-method
|
||||||
@test "REST - service route - invalid POST route" {
|
@test "REST - service route - invalid POST route" {
|
||||||
if [ "$DIRECT" != "true" ]; then
|
local bucket_name
|
||||||
skip "https://github.com/versity/versitygw/issues/1810"
|
|
||||||
fi
|
|
||||||
run get_file_name
|
|
||||||
assert_success
|
|
||||||
file_name=$output
|
|
||||||
|
|
||||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
run setup_bucket_v3 "$BUCKET_ONE_NAME"
|
||||||
assert_success
|
assert_success
|
||||||
bucket_name="$output"
|
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"
|
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
|
assert_success
|
||||||
}
|
}
|
||||||
@@ -349,32 +338,24 @@ export RUN_USERS=true
|
|||||||
|
|
||||||
# tags: curl,ListBuckets,required-headers,x-amz-date
|
# tags: curl,ListBuckets,required-headers,x-amz-date
|
||||||
@test "REST - ListBuckets - omit date" {
|
@test "REST - ListBuckets - omit date" {
|
||||||
if [ "$DIRECT" != "true" ]; then
|
local bucket_name
|
||||||
skip "https://github.com/versity/versitygw/issues/1934"
|
|
||||||
fi
|
run setup_bucket_v3 "$BUCKET_ONE_NAME"
|
||||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
|
||||||
assert_success
|
assert_success
|
||||||
bucket_name="$output"
|
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"
|
run send_rest_go_command_expect_error "403" "AccessDenied" "AWS authentication requires a valid Date or x-amz-date header" "-omitDate"
|
||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
|
|
||||||
# tags: curl,ListBuckets,required-headers,x-amz-date
|
# tags: curl,ListBuckets,required-headers,x-amz-date
|
||||||
@test "REST - ListBuckets - invalid date" {
|
@test "REST - ListBuckets - invalid date" {
|
||||||
if [ "$DIRECT" != "true" ]; then
|
local bucket_name
|
||||||
skip "https://github.com/versity/versitygw/issues/1934"
|
|
||||||
fi
|
run setup_bucket_v3 "$BUCKET_ONE_NAME"
|
||||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
|
||||||
assert_success
|
assert_success
|
||||||
bucket_name="$output"
|
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"
|
run send_rest_go_command_expect_error "403" "AccessDenied" "AWS authentication requires a valid Date or x-amz-date header" "-customDate" "ABCDEFG"
|
||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
@@ -122,9 +122,6 @@ source ./tests/drivers/objects_and_versions.sh
|
|||||||
|
|
||||||
# tags: curl,ListObjects,minimal-request
|
# tags: curl,ListObjects,minimal-request
|
||||||
@test "REST - list objects v1 - no NextMarker without delimiter" {
|
@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"
|
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||||
assert_success
|
assert_success
|
||||||
bucket_name="$output"
|
bucket_name="$output"
|
||||||
|
|||||||
Reference in new issue
Block a user