diff --git a/auth/acl.go b/auth/acl.go
index b51742b..8bcb00c 100644
--- a/auth/acl.go
+++ b/auth/acl.go
@@ -28,7 +28,6 @@ import (
)
type ACL struct {
- ACL types.BucketCannedACL
Owner string
Grantees []Grantee
}
@@ -94,12 +93,34 @@ func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, er
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
}
+ defaultGrantees := []Grantee{
+ {
+ Permission: types.PermissionFullControl,
+ Access: acl.Owner,
+ },
+ }
+
// if the ACL is specified, set the ACL, else replace the grantees
if input.ACL != "" {
- acl.ACL = input.ACL
- acl.Grantees = []Grantee{}
+ switch input.ACL {
+ case types.BucketCannedACLPublicRead:
+ defaultGrantees = append(defaultGrantees, Grantee{
+ Permission: types.PermissionRead,
+ Access: "all-users",
+ })
+ case types.BucketCannedACLPublicReadWrite:
+ defaultGrantees = append(defaultGrantees, []Grantee{
+ {
+ Permission: types.PermissionRead,
+ Access: "all-users",
+ },
+ {
+ Permission: types.PermissionWrite,
+ Access: "all-users",
+ },
+ }...)
+ }
} else {
- grantees := []Grantee{}
accs := []string{}
if input.GrantRead != nil || input.GrantReadACP != nil || input.GrantFullControl != nil || input.GrantWrite != nil || input.GrantWriteACP != nil {
@@ -108,31 +129,31 @@ func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, er
if input.GrantFullControl != nil && *input.GrantFullControl != "" {
fullControlList = splitUnique(*input.GrantFullControl, ",")
for _, str := range fullControlList {
- grantees = append(grantees, Grantee{Access: str, Permission: "FULL_CONTROL"})
+ defaultGrantees = append(defaultGrantees, Grantee{Access: str, Permission: "FULL_CONTROL"})
}
}
if input.GrantRead != nil && *input.GrantRead != "" {
readList = splitUnique(*input.GrantRead, ",")
for _, str := range readList {
- grantees = append(grantees, Grantee{Access: str, Permission: "READ"})
+ defaultGrantees = append(defaultGrantees, Grantee{Access: str, Permission: "READ"})
}
}
if input.GrantReadACP != nil && *input.GrantReadACP != "" {
readACPList = splitUnique(*input.GrantReadACP, ",")
for _, str := range readACPList {
- grantees = append(grantees, Grantee{Access: str, Permission: "READ_ACP"})
+ defaultGrantees = append(defaultGrantees, Grantee{Access: str, Permission: "READ_ACP"})
}
}
if input.GrantWrite != nil && *input.GrantWrite != "" {
writeList = splitUnique(*input.GrantWrite, ",")
for _, str := range writeList {
- grantees = append(grantees, Grantee{Access: str, Permission: "WRITE"})
+ defaultGrantees = append(defaultGrantees, Grantee{Access: str, Permission: "WRITE"})
}
}
if input.GrantWriteACP != nil && *input.GrantWriteACP != "" {
writeACPList = splitUnique(*input.GrantWriteACP, ",")
for _, str := range writeACPList {
- grantees = append(grantees, Grantee{Access: str, Permission: "WRITE_ACP"})
+ defaultGrantees = append(defaultGrantees, Grantee{Access: str, Permission: "WRITE_ACP"})
}
}
@@ -143,7 +164,7 @@ func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, er
if grt.Grantee == nil || grt.Grantee.ID == nil || grt.Permission == "" {
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
}
- grantees = append(grantees, Grantee{Access: *grt.Grantee.ID, Permission: grt.Permission})
+ defaultGrantees = append(defaultGrantees, Grantee{Access: *grt.Grantee.ID, Permission: grt.Permission})
if _, ok := cache[*grt.Grantee.ID]; !ok {
cache[*grt.Grantee.ID] = true
accs = append(accs, *grt.Grantee.ID)
@@ -159,11 +180,10 @@ func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, er
if len(accList) > 0 {
return nil, fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", "))
}
-
- acl.Grantees = grantees
- acl.ACL = ""
}
+ acl.Grantees = defaultGrantees
+
result, err := json.Marshal(acl)
if err != nil {
return nil, err
@@ -207,34 +227,21 @@ func splitUnique(s, divider string) []string {
}
func verifyACL(acl ACL, access string, permission types.Permission) error {
- if acl.ACL != "" {
- if (permission == "READ" || permission == "READ_ACP") && (acl.ACL != "public-read" && acl.ACL != "public-read-write") {
- return s3err.GetAPIError(s3err.ErrAccessDenied)
- }
- if (permission == "WRITE" || permission == "WRITE_ACP") && acl.ACL != "public-read-write" {
- return s3err.GetAPIError(s3err.ErrAccessDenied)
- }
+ grantee := Grantee{Access: access, Permission: permission}
+ granteeFullCtrl := Grantee{Access: access, Permission: "FULL_CONTROL"}
+ granteeAllUsers := Grantee{Access: "all-users", Permission: permission}
+ isFound := false
+
+ for _, grt := range acl.Grantees {
+ if grt == grantee || grt == granteeFullCtrl || grt == granteeAllUsers {
+ isFound = true
+ break
+ }
+ }
+
+ if isFound {
return nil
- } else {
- if len(acl.Grantees) == 0 {
- return nil
- }
- grantee := Grantee{Access: access, Permission: permission}
- granteeFullCtrl := Grantee{Access: access, Permission: "FULL_CONTROL"}
-
- isFound := false
-
- for _, grt := range acl.Grantees {
- if grt == grantee || grt == granteeFullCtrl {
- isFound = true
- break
- }
- }
-
- if isFound {
- return nil
- }
}
return s3err.GetAPIError(s3err.ErrAccessDenied)
@@ -295,23 +302,16 @@ func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) e
if opts.Acc.Role == RoleAdmin {
return nil
}
- if opts.Acc.Access == opts.Acl.Owner {
- return nil
- }
policy, policyErr := be.GetBucketPolicy(ctx, opts.Bucket)
- if policyErr != nil && !errors.Is(policyErr, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
- return policyErr
+ if policyErr != nil {
+ if !errors.Is(policyErr, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
+ return policyErr
+ }
+ } else {
+ return VerifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, opts.Action)
}
- // If bucket policy is not set and the ACL is default, only the owner has access
- if errors.Is(policyErr, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) && opts.Acl.ACL == "" && len(opts.Acl.Grantees) == 0 {
- return s3err.GetAPIError(s3err.ErrAccessDenied)
- }
-
- if err := VerifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, opts.Action); err != nil {
- return err
- }
if err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission); err != nil {
return err
}
diff --git a/auth/bucket_policy.go b/auth/bucket_policy.go
index ff0cebb..0765d27 100644
--- a/auth/bucket_policy.go
+++ b/auth/bucket_policy.go
@@ -134,11 +134,6 @@ func ValidatePolicyDocument(policyBin []byte, bucket string, iam IAMService) err
}
func VerifyBucketPolicy(policy []byte, access, bucket, object string, action Action) error {
- // If bucket policy is not set
- if policy == nil {
- return nil
- }
-
var bucketPolicy BucketPolicy
if err := json.Unmarshal(policy, &bucketPolicy); err != nil {
return err
diff --git a/auth/bucket_policy_actions.go b/auth/bucket_policy_actions.go
index 10ffd9e..22510dc 100644
--- a/auth/bucket_policy_actions.go
+++ b/auth/bucket_policy_actions.go
@@ -55,6 +55,8 @@ const (
GetObjectRetentionAction Action = "s3:GetObjectRetention"
PutObjectRetentionAction Action = "s3:PutObjectRetention"
BypassGovernanceRetentionAction Action = "s3:BypassGovernanceRetention"
+ PutBucketOwnershipControlsAction Action = "s3:PutBucketOwnershipControls"
+ GetBucketOwnershipControlsAction Action = "s3:GetBucketOwnershipControls"
AllActions Action = "s3:*"
)
@@ -91,6 +93,8 @@ var supportedActionList = map[Action]struct{}{
GetObjectRetentionAction: {},
PutObjectRetentionAction: {},
BypassGovernanceRetentionAction: {},
+ PutBucketOwnershipControlsAction: {},
+ GetBucketOwnershipControlsAction: {},
AllActions: {},
}
diff --git a/backend/azure/azure.go b/backend/azure/azure.go
index bedbffe..b0b117d 100644
--- a/backend/azure/azure.go
+++ b/backend/azure/azure.go
@@ -53,6 +53,7 @@ type key string
const (
keyAclCapital key = "Acl"
keyAclLower key = "acl"
+ keyOwnership key = "Ownership"
keyTags key = "Tags"
keyPolicy key = "Policy"
keyBucketLock key = "Bucket-Lock"
@@ -127,6 +128,7 @@ func (az *Azure) String() string {
func (az *Azure) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, acl []byte) error {
meta := map[string]*string{
string(keyAclCapital): backend.GetStringPtr(string(acl)),
+ string(keyOwnership): backend.GetStringPtr(string(input.ObjectOwnership)),
}
acct, ok := ctx.Value("account").(auth.Account)
@@ -251,6 +253,67 @@ func (az *Azure) DeleteBucket(ctx context.Context, input *s3.DeleteBucketInput)
return azureErrToS3Err(err)
}
+func (az *Azure) PutBucketOwnershipControls(ctx context.Context, bucket string, ownership types.ObjectOwnership) error {
+ client, err := az.getContainerClient(bucket)
+ if err != nil {
+ return err
+ }
+
+ resp, err := client.GetProperties(ctx, &container.GetPropertiesOptions{})
+ if err != nil {
+ return azureErrToS3Err(err)
+ }
+ resp.Metadata[string(keyOwnership)] = backend.GetStringPtr(string(ownership))
+
+ _, err = client.SetMetadata(ctx, &container.SetMetadataOptions{Metadata: resp.Metadata})
+ if err != nil {
+ return azureErrToS3Err(err)
+ }
+
+ return nil
+}
+
+func (az *Azure) GetBucketOwnershipControls(ctx context.Context, bucket string) (types.ObjectOwnership, error) {
+ var ownship types.ObjectOwnership
+ client, err := az.getContainerClient(bucket)
+ if err != nil {
+ return ownship, err
+ }
+
+ resp, err := client.GetProperties(ctx, &container.GetPropertiesOptions{})
+ if err != nil {
+ return ownship, azureErrToS3Err(err)
+ }
+
+ ownership, ok := resp.Metadata[string(keyOwnership)]
+ if !ok {
+ return ownship, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)
+ }
+
+ return types.ObjectOwnership(*ownership), nil
+}
+
+func (az *Azure) DeleteBucketOwnershipControls(ctx context.Context, bucket string) error {
+ client, err := az.getContainerClient(bucket)
+ if err != nil {
+ return err
+ }
+
+ resp, err := client.GetProperties(ctx, &container.GetPropertiesOptions{})
+ if err != nil {
+ return azureErrToS3Err(err)
+ }
+
+ delete(resp.Metadata, string(keyOwnership))
+
+ _, err = client.SetMetadata(ctx, &container.SetMetadataOptions{Metadata: resp.Metadata})
+ if err != nil {
+ return azureErrToS3Err(err)
+ }
+
+ return nil
+}
+
func (az *Azure) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, error) {
tags, err := parseTags(po.Tagging)
if err != nil {
diff --git a/backend/backend.go b/backend/backend.go
index 5fe7078..6ebc705 100644
--- a/backend/backend.go
+++ b/backend/backend.go
@@ -21,6 +21,7 @@ import (
"io"
"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"
"github.com/versity/versitygw/s3select"
@@ -43,6 +44,9 @@ type Backend interface {
PutBucketPolicy(_ context.Context, bucket string, policy []byte) error
GetBucketPolicy(_ context.Context, bucket string) ([]byte, error)
DeleteBucketPolicy(_ context.Context, bucket string) error
+ PutBucketOwnershipControls(_ context.Context, bucket string, ownership types.ObjectOwnership) error
+ GetBucketOwnershipControls(_ context.Context, bucket string) (types.ObjectOwnership, error)
+ DeleteBucketOwnershipControls(_ context.Context, bucket string) error
// multipart operations
CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
@@ -138,6 +142,15 @@ func (BackendUnsupported) GetBucketPolicy(_ context.Context, bucket string) ([]b
func (BackendUnsupported) DeleteBucketPolicy(_ context.Context, bucket string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
+func (BackendUnsupported) PutBucketOwnershipControls(_ context.Context, bucket string, ownership types.ObjectOwnership) error {
+ return s3err.GetAPIError(s3err.ErrNotImplemented)
+}
+func (BackendUnsupported) GetBucketOwnershipControls(_ context.Context, bucket string) (types.ObjectOwnership, error) {
+ return types.ObjectOwnershipBucketOwnerEnforced, s3err.GetAPIError(s3err.ErrNotImplemented)
+}
+func (BackendUnsupported) DeleteBucketOwnershipControls(_ context.Context, bucket string) error {
+ return s3err.GetAPIError(s3err.ErrNotImplemented)
+}
func (BackendUnsupported) CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
diff --git a/backend/posix/posix.go b/backend/posix/posix.go
index 65201fb..f92d0fc 100644
--- a/backend/posix/posix.go
+++ b/backend/posix/posix.go
@@ -75,6 +75,7 @@ const (
contentEncHdr = "content-encoding"
emptyMD5 = "d41d8cd98f00b204e9800998ecf8427e"
aclkey = "acl"
+ ownershipkey = "ownership"
etagkey = "etag"
policykey = "policy"
bucketLockKey = "bucket-lock"
@@ -245,6 +246,9 @@ func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, a
if err := p.meta.StoreAttribute(bucket, "", aclkey, acl); err != nil {
return fmt.Errorf("set acl: %w", err)
}
+ if err := p.meta.StoreAttribute(bucket, "", ownershipkey, []byte(input.ObjectOwnership)); err != nil {
+ return fmt.Errorf("set ownership: %w", err)
+ }
if input.ObjectLockEnabledForBucket != nil && *input.ObjectLockEnabledForBucket {
now := time.Now()
@@ -304,6 +308,61 @@ func (p *Posix) DeleteBucket(_ context.Context, input *s3.DeleteBucketInput) err
return nil
}
+func (p *Posix) PutBucketOwnershipControls(_ context.Context, bucket string, ownership types.ObjectOwnership) error {
+ _, err := os.Stat(bucket)
+ if errors.Is(err, fs.ErrNotExist) {
+ return s3err.GetAPIError(s3err.ErrNoSuchBucket)
+ }
+ if err != nil {
+ return fmt.Errorf("stat bucket: %w", err)
+ }
+
+ if err := p.meta.StoreAttribute(bucket, "", ownershipkey, []byte(ownership)); err != nil {
+ return fmt.Errorf("set ownership: %w", err)
+ }
+
+ return nil
+}
+func (p *Posix) GetBucketOwnershipControls(_ context.Context, bucket string) (types.ObjectOwnership, error) {
+ var ownship types.ObjectOwnership
+ _, err := os.Stat(bucket)
+ if errors.Is(err, fs.ErrNotExist) {
+ return ownship, s3err.GetAPIError(s3err.ErrNoSuchBucket)
+ }
+ if err != nil {
+ return ownship, fmt.Errorf("stat bucket: %w", err)
+ }
+
+ ownership, err := p.meta.RetrieveAttribute(bucket, "", ownershipkey)
+ if errors.Is(err, meta.ErrNoSuchKey) {
+ return ownship, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)
+ }
+ if err != nil {
+ return ownship, fmt.Errorf("get bucket ownership status: %w", err)
+ }
+
+ return types.ObjectOwnership(ownership), nil
+}
+func (p *Posix) DeleteBucketOwnershipControls(_ context.Context, bucket string) error {
+ _, err := os.Stat(bucket)
+ if errors.Is(err, fs.ErrNotExist) {
+ return s3err.GetAPIError(s3err.ErrNoSuchBucket)
+ }
+ if err != nil {
+ return fmt.Errorf("stat bucket: %w", err)
+ }
+
+ if err := p.meta.DeleteAttribute(bucket, "", ownershipkey); err != nil {
+ if errors.Is(err, meta.ErrNoSuchKey) {
+ return nil
+ }
+
+ return fmt.Errorf("delete ownership: %w", err)
+ }
+
+ return nil
+}
+
func (p *Posix) CreateMultipartUpload(ctx context.Context, mpu *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
if mpu.Bucket == nil {
return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName)
diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go
index 98ffd8c..6b2010c 100644
--- a/backend/s3proxy/s3.go
+++ b/backend/s3proxy/s3.go
@@ -55,6 +55,8 @@ type S3Proxy struct {
debug bool
}
+var _ backend.Backend = &S3Proxy{}
+
func New(access, secret, endpoint, region string, disableChecksum, sslSkipVerify, debug bool) (*S3Proxy, error) {
s := &S3Proxy{
access: access,
@@ -128,6 +130,37 @@ func (s *S3Proxy) DeleteBucket(ctx context.Context, input *s3.DeleteBucketInput)
return handleError(err)
}
+func (s *S3Proxy) PutBucketOwnershipControls(ctx context.Context, bucket string, ownership types.ObjectOwnership) error {
+ _, err := s.client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ OwnershipControls: &types.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: ownership,
+ },
+ },
+ },
+ })
+ return handleError(err)
+}
+
+func (s *S3Proxy) GetBucketOwnershipControls(ctx context.Context, bucket string) (types.ObjectOwnership, error) {
+ var ownship types.ObjectOwnership
+ resp, err := s.client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ })
+ if err != nil {
+ return ownship, handleError(err)
+ }
+ return resp.OwnershipControls.Rules[0].ObjectOwnership, nil
+}
+func (s *S3Proxy) DeleteBucketOwnershipControls(ctx context.Context, bucket string) error {
+ _, err := s.client.DeleteBucketOwnershipControls(ctx, &s3.DeleteBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ })
+ return handleError(err)
+}
+
func (s *S3Proxy) CreateMultipartUpload(ctx context.Context, input *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
out, err := s.client.CreateMultipartUpload(ctx, input)
return out, handleError(err)
diff --git a/metrics/actions.go b/metrics/actions.go
index 4a31d81..36d5ad8 100644
--- a/metrics/actions.go
+++ b/metrics/actions.go
@@ -24,51 +24,54 @@ var (
)
var (
- ActionUndetected = "ActionUnDetected"
- ActionAbortMultipartUpload = "s3_AbortMultipartUpload"
- ActionCompleteMultipartUpload = "s3_CompleteMultipartUpload"
- ActionCopyObject = "s3_CopyObject"
- ActionCreateBucket = "s3_CreateBucket"
- ActionCreateMultipartUpload = "s3_CreateMultipartUpload"
- ActionDeleteBucket = "s3_DeleteBucket"
- ActionDeleteBucketPolicy = "s3_DeleteBucketPolicy"
- ActionDeleteBucketTagging = "s3_DeleteBucketTagging"
- ActionDeleteObject = "s3_DeleteObject"
- ActionDeleteObjectTagging = "s3_DeleteObjectTagging"
- ActionDeleteObjects = "s3_DeleteObjects"
- ActionGetBucketAcl = "s3_GetBucketAcl"
- ActionGetBucketPolicy = "s3_GetBucketPolicy"
- ActionGetBucketTagging = "s3_GetBucketTagging"
- ActionGetBucketVersioning = "s3_GetBucketVersioning"
- ActionGetObject = "s3_GetObject"
- ActionGetObjectAcl = "s3_GetObjectAcl"
- ActionGetObjectAttributes = "s3_GetObjectAttributes"
- ActionGetObjectLegalHold = "s3_GetObjectLegalHold"
- ActionGetObjectLockConfiguration = "s3_GetObjectLockConfiguration"
- ActionGetObjectRetention = "s3_GetObjectRetention"
- ActionGetObjectTagging = "s3_GetObjectTagging"
- ActionHeadBucket = "s3_HeadBucket"
- ActionHeadObject = "s3_HeadObject"
- ActionListAllMyBuckets = "s3_ListAllMyBuckets"
- ActionListMultipartUploads = "s3_ListMultipartUploads"
- ActionListObjectVersions = "s3_ListObjectVersions"
- ActionListObjects = "s3_ListObjects"
- ActionListObjectsV2 = "s3_ListObjectsV2"
- ActionListParts = "s3_ListParts"
- ActionPutBucketAcl = "s3_PutBucketAcl"
- ActionPutBucketPolicy = "s3_PutBucketPolicy"
- ActionPutBucketTagging = "s3_PutBucketTagging"
- ActionPutBucketVersioning = "s3_PutBucketVersioning"
- ActionPutObject = "s3_PutObject"
- ActionPutObjectAcl = "s3_PutObjectAcl"
- ActionPutObjectLegalHold = "s3_PutObjectLegalHold"
- ActionPutObjectLockConfiguration = "s3_PutObjectLockConfiguration"
- ActionPutObjectRetention = "s3_PutObjectRetention"
- ActionPutObjectTagging = "s3_PutObjectTagging"
- ActionRestoreObject = "s3_RestoreObject"
- ActionSelectObjectContent = "s3_SelectObjectContent"
- ActionUploadPart = "s3_UploadPart"
- ActionUploadPartCopy = "s3_UploadPartCopy"
+ ActionUndetected = "ActionUnDetected"
+ ActionAbortMultipartUpload = "s3_AbortMultipartUpload"
+ ActionCompleteMultipartUpload = "s3_CompleteMultipartUpload"
+ ActionCopyObject = "s3_CopyObject"
+ ActionCreateBucket = "s3_CreateBucket"
+ ActionCreateMultipartUpload = "s3_CreateMultipartUpload"
+ ActionDeleteBucket = "s3_DeleteBucket"
+ ActionDeleteBucketPolicy = "s3_DeleteBucketPolicy"
+ ActionDeleteBucketTagging = "s3_DeleteBucketTagging"
+ ActionDeleteObject = "s3_DeleteObject"
+ ActionDeleteObjectTagging = "s3_DeleteObjectTagging"
+ ActionDeleteObjects = "s3_DeleteObjects"
+ ActionGetBucketAcl = "s3_GetBucketAcl"
+ ActionGetBucketPolicy = "s3_GetBucketPolicy"
+ ActionGetBucketTagging = "s3_GetBucketTagging"
+ ActionGetBucketVersioning = "s3_GetBucketVersioning"
+ ActionGetObject = "s3_GetObject"
+ ActionGetObjectAcl = "s3_GetObjectAcl"
+ ActionGetObjectAttributes = "s3_GetObjectAttributes"
+ ActionGetObjectLegalHold = "s3_GetObjectLegalHold"
+ ActionGetObjectLockConfiguration = "s3_GetObjectLockConfiguration"
+ ActionGetObjectRetention = "s3_GetObjectRetention"
+ ActionGetObjectTagging = "s3_GetObjectTagging"
+ ActionHeadBucket = "s3_HeadBucket"
+ ActionHeadObject = "s3_HeadObject"
+ ActionListAllMyBuckets = "s3_ListAllMyBuckets"
+ ActionListMultipartUploads = "s3_ListMultipartUploads"
+ ActionListObjectVersions = "s3_ListObjectVersions"
+ ActionListObjects = "s3_ListObjects"
+ ActionListObjectsV2 = "s3_ListObjectsV2"
+ ActionListParts = "s3_ListParts"
+ ActionPutBucketAcl = "s3_PutBucketAcl"
+ ActionPutBucketPolicy = "s3_PutBucketPolicy"
+ ActionPutBucketTagging = "s3_PutBucketTagging"
+ ActionPutBucketVersioning = "s3_PutBucketVersioning"
+ ActionPutObject = "s3_PutObject"
+ ActionPutObjectAcl = "s3_PutObjectAcl"
+ ActionPutObjectLegalHold = "s3_PutObjectLegalHold"
+ ActionPutObjectLockConfiguration = "s3_PutObjectLockConfiguration"
+ ActionPutObjectRetention = "s3_PutObjectRetention"
+ ActionPutObjectTagging = "s3_PutObjectTagging"
+ ActionRestoreObject = "s3_RestoreObject"
+ ActionSelectObjectContent = "s3_SelectObjectContent"
+ ActionUploadPart = "s3_UploadPart"
+ ActionUploadPartCopy = "s3_UploadPartCopy"
+ ActionPutBucketOwnershipControls = "s3_PutBucketOwnershipControls"
+ ActionGetBucketOwnershipControls = "s3_GetBucketOwnershipControls"
+ ActionDeleteBucketOwnershipControls = "s3_DeleteBucketOwnershipControls"
)
func init() {
diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go
index 12583c6..3a8aef3 100644
--- a/s3api/controllers/backend_moq_test.go
+++ b/s3api/controllers/backend_moq_test.go
@@ -7,6 +7,7 @@ import (
"bufio"
"context"
"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/s3response"
"io"
@@ -44,6 +45,9 @@ var _ backend.Backend = &BackendMock{}
// DeleteBucketFunc: func(contextMoqParam context.Context, deleteBucketInput *s3.DeleteBucketInput) error {
// panic("mock out the DeleteBucket method")
// },
+// DeleteBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) error {
+// panic("mock out the DeleteBucketOwnershipControls method")
+// },
// DeleteBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) error {
// panic("mock out the DeleteBucketPolicy method")
// },
@@ -62,6 +66,9 @@ var _ backend.Backend = &BackendMock{}
// GetBucketAclFunc: func(contextMoqParam context.Context, getBucketAclInput *s3.GetBucketAclInput) ([]byte, error) {
// panic("mock out the GetBucketAcl method")
// },
+// GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
+// panic("mock out the GetBucketOwnershipControls method")
+// },
// GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
// panic("mock out the GetBucketPolicy method")
// },
@@ -122,6 +129,9 @@ var _ backend.Backend = &BackendMock{}
// PutBucketAclFunc: func(contextMoqParam context.Context, bucket string, data []byte) error {
// panic("mock out the PutBucketAcl method")
// },
+// PutBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error {
+// panic("mock out the PutBucketOwnershipControls method")
+// },
// PutBucketPolicyFunc: func(contextMoqParam context.Context, bucket string, policy []byte) error {
// panic("mock out the PutBucketPolicy method")
// },
@@ -195,6 +205,9 @@ type BackendMock struct {
// DeleteBucketFunc mocks the DeleteBucket method.
DeleteBucketFunc func(contextMoqParam context.Context, deleteBucketInput *s3.DeleteBucketInput) error
+ // DeleteBucketOwnershipControlsFunc mocks the DeleteBucketOwnershipControls method.
+ DeleteBucketOwnershipControlsFunc func(contextMoqParam context.Context, bucket string) error
+
// DeleteBucketPolicyFunc mocks the DeleteBucketPolicy method.
DeleteBucketPolicyFunc func(contextMoqParam context.Context, bucket string) error
@@ -213,6 +226,9 @@ type BackendMock struct {
// GetBucketAclFunc mocks the GetBucketAcl method.
GetBucketAclFunc func(contextMoqParam context.Context, getBucketAclInput *s3.GetBucketAclInput) ([]byte, error)
+ // GetBucketOwnershipControlsFunc mocks the GetBucketOwnershipControls method.
+ GetBucketOwnershipControlsFunc func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error)
+
// GetBucketPolicyFunc mocks the GetBucketPolicy method.
GetBucketPolicyFunc func(contextMoqParam context.Context, bucket string) ([]byte, error)
@@ -273,6 +289,9 @@ type BackendMock struct {
// PutBucketAclFunc mocks the PutBucketAcl method.
PutBucketAclFunc func(contextMoqParam context.Context, bucket string, data []byte) error
+ // PutBucketOwnershipControlsFunc mocks the PutBucketOwnershipControls method.
+ PutBucketOwnershipControlsFunc func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error
+
// PutBucketPolicyFunc mocks the PutBucketPolicy method.
PutBucketPolicyFunc func(contextMoqParam context.Context, bucket string, policy []byte) error
@@ -373,6 +392,13 @@ type BackendMock struct {
// DeleteBucketInput is the deleteBucketInput argument value.
DeleteBucketInput *s3.DeleteBucketInput
}
+ // DeleteBucketOwnershipControls holds details about calls to the DeleteBucketOwnershipControls method.
+ DeleteBucketOwnershipControls []struct {
+ // ContextMoqParam is the contextMoqParam argument value.
+ ContextMoqParam context.Context
+ // Bucket is the bucket argument value.
+ Bucket string
+ }
// DeleteBucketPolicy holds details about calls to the DeleteBucketPolicy method.
DeleteBucketPolicy []struct {
// ContextMoqParam is the contextMoqParam argument value.
@@ -417,6 +443,13 @@ type BackendMock struct {
// GetBucketAclInput is the getBucketAclInput argument value.
GetBucketAclInput *s3.GetBucketAclInput
}
+ // GetBucketOwnershipControls holds details about calls to the GetBucketOwnershipControls method.
+ GetBucketOwnershipControls []struct {
+ // ContextMoqParam is the contextMoqParam argument value.
+ ContextMoqParam context.Context
+ // Bucket is the bucket argument value.
+ Bucket string
+ }
// GetBucketPolicy holds details about calls to the GetBucketPolicy method.
GetBucketPolicy []struct {
// ContextMoqParam is the contextMoqParam argument value.
@@ -571,6 +604,15 @@ type BackendMock struct {
// Data is the data argument value.
Data []byte
}
+ // PutBucketOwnershipControls holds details about calls to the PutBucketOwnershipControls method.
+ PutBucketOwnershipControls []struct {
+ // ContextMoqParam is the contextMoqParam argument value.
+ ContextMoqParam context.Context
+ // Bucket is the bucket argument value.
+ Bucket string
+ // Ownership is the ownership argument value.
+ Ownership types.ObjectOwnership
+ }
// PutBucketPolicy holds details about calls to the PutBucketPolicy method.
PutBucketPolicy []struct {
// ContextMoqParam is the contextMoqParam argument value.
@@ -693,54 +735,57 @@ type BackendMock struct {
UploadPartCopyInput *s3.UploadPartCopyInput
}
}
- lockAbortMultipartUpload sync.RWMutex
- lockChangeBucketOwner sync.RWMutex
- lockCompleteMultipartUpload sync.RWMutex
- lockCopyObject sync.RWMutex
- lockCreateBucket sync.RWMutex
- lockCreateMultipartUpload sync.RWMutex
- lockDeleteBucket sync.RWMutex
- lockDeleteBucketPolicy sync.RWMutex
- lockDeleteBucketTagging sync.RWMutex
- lockDeleteObject sync.RWMutex
- lockDeleteObjectTagging sync.RWMutex
- lockDeleteObjects sync.RWMutex
- lockGetBucketAcl sync.RWMutex
- lockGetBucketPolicy sync.RWMutex
- lockGetBucketTagging sync.RWMutex
- lockGetBucketVersioning sync.RWMutex
- lockGetObject sync.RWMutex
- lockGetObjectAcl sync.RWMutex
- lockGetObjectAttributes sync.RWMutex
- lockGetObjectLegalHold sync.RWMutex
- lockGetObjectLockConfiguration sync.RWMutex
- lockGetObjectRetention sync.RWMutex
- lockGetObjectTagging sync.RWMutex
- lockHeadBucket sync.RWMutex
- lockHeadObject sync.RWMutex
- lockListBuckets sync.RWMutex
- lockListBucketsAndOwners sync.RWMutex
- lockListMultipartUploads sync.RWMutex
- lockListObjectVersions sync.RWMutex
- lockListObjects sync.RWMutex
- lockListObjectsV2 sync.RWMutex
- lockListParts sync.RWMutex
- lockPutBucketAcl sync.RWMutex
- lockPutBucketPolicy sync.RWMutex
- lockPutBucketTagging sync.RWMutex
- lockPutBucketVersioning sync.RWMutex
- lockPutObject sync.RWMutex
- lockPutObjectAcl sync.RWMutex
- lockPutObjectLegalHold sync.RWMutex
- lockPutObjectLockConfiguration sync.RWMutex
- lockPutObjectRetention sync.RWMutex
- lockPutObjectTagging sync.RWMutex
- lockRestoreObject sync.RWMutex
- lockSelectObjectContent sync.RWMutex
- lockShutdown sync.RWMutex
- lockString sync.RWMutex
- lockUploadPart sync.RWMutex
- lockUploadPartCopy sync.RWMutex
+ lockAbortMultipartUpload sync.RWMutex
+ lockChangeBucketOwner sync.RWMutex
+ lockCompleteMultipartUpload sync.RWMutex
+ lockCopyObject sync.RWMutex
+ lockCreateBucket sync.RWMutex
+ lockCreateMultipartUpload sync.RWMutex
+ lockDeleteBucket sync.RWMutex
+ lockDeleteBucketOwnershipControls sync.RWMutex
+ lockDeleteBucketPolicy sync.RWMutex
+ lockDeleteBucketTagging sync.RWMutex
+ lockDeleteObject sync.RWMutex
+ lockDeleteObjectTagging sync.RWMutex
+ lockDeleteObjects sync.RWMutex
+ lockGetBucketAcl sync.RWMutex
+ lockGetBucketOwnershipControls sync.RWMutex
+ lockGetBucketPolicy sync.RWMutex
+ lockGetBucketTagging sync.RWMutex
+ lockGetBucketVersioning sync.RWMutex
+ lockGetObject sync.RWMutex
+ lockGetObjectAcl sync.RWMutex
+ lockGetObjectAttributes sync.RWMutex
+ lockGetObjectLegalHold sync.RWMutex
+ lockGetObjectLockConfiguration sync.RWMutex
+ lockGetObjectRetention sync.RWMutex
+ lockGetObjectTagging sync.RWMutex
+ lockHeadBucket sync.RWMutex
+ lockHeadObject sync.RWMutex
+ lockListBuckets sync.RWMutex
+ lockListBucketsAndOwners sync.RWMutex
+ lockListMultipartUploads sync.RWMutex
+ lockListObjectVersions sync.RWMutex
+ lockListObjects sync.RWMutex
+ lockListObjectsV2 sync.RWMutex
+ lockListParts sync.RWMutex
+ lockPutBucketAcl sync.RWMutex
+ lockPutBucketOwnershipControls sync.RWMutex
+ lockPutBucketPolicy sync.RWMutex
+ lockPutBucketTagging sync.RWMutex
+ lockPutBucketVersioning sync.RWMutex
+ lockPutObject sync.RWMutex
+ lockPutObjectAcl sync.RWMutex
+ lockPutObjectLegalHold sync.RWMutex
+ lockPutObjectLockConfiguration sync.RWMutex
+ lockPutObjectRetention sync.RWMutex
+ lockPutObjectTagging sync.RWMutex
+ lockRestoreObject sync.RWMutex
+ lockSelectObjectContent sync.RWMutex
+ lockShutdown sync.RWMutex
+ lockString sync.RWMutex
+ lockUploadPart sync.RWMutex
+ lockUploadPartCopy sync.RWMutex
}
// AbortMultipartUpload calls AbortMultipartUploadFunc.
@@ -1003,6 +1048,42 @@ func (mock *BackendMock) DeleteBucketCalls() []struct {
return calls
}
+// DeleteBucketOwnershipControls calls DeleteBucketOwnershipControlsFunc.
+func (mock *BackendMock) DeleteBucketOwnershipControls(contextMoqParam context.Context, bucket string) error {
+ if mock.DeleteBucketOwnershipControlsFunc == nil {
+ panic("BackendMock.DeleteBucketOwnershipControlsFunc: method is nil but Backend.DeleteBucketOwnershipControls was just called")
+ }
+ callInfo := struct {
+ ContextMoqParam context.Context
+ Bucket string
+ }{
+ ContextMoqParam: contextMoqParam,
+ Bucket: bucket,
+ }
+ mock.lockDeleteBucketOwnershipControls.Lock()
+ mock.calls.DeleteBucketOwnershipControls = append(mock.calls.DeleteBucketOwnershipControls, callInfo)
+ mock.lockDeleteBucketOwnershipControls.Unlock()
+ return mock.DeleteBucketOwnershipControlsFunc(contextMoqParam, bucket)
+}
+
+// DeleteBucketOwnershipControlsCalls gets all the calls that were made to DeleteBucketOwnershipControls.
+// Check the length with:
+//
+// len(mockedBackend.DeleteBucketOwnershipControlsCalls())
+func (mock *BackendMock) DeleteBucketOwnershipControlsCalls() []struct {
+ ContextMoqParam context.Context
+ Bucket string
+} {
+ var calls []struct {
+ ContextMoqParam context.Context
+ Bucket string
+ }
+ mock.lockDeleteBucketOwnershipControls.RLock()
+ calls = mock.calls.DeleteBucketOwnershipControls
+ mock.lockDeleteBucketOwnershipControls.RUnlock()
+ return calls
+}
+
// DeleteBucketPolicy calls DeleteBucketPolicyFunc.
func (mock *BackendMock) DeleteBucketPolicy(contextMoqParam context.Context, bucket string) error {
if mock.DeleteBucketPolicyFunc == nil {
@@ -1223,6 +1304,42 @@ func (mock *BackendMock) GetBucketAclCalls() []struct {
return calls
}
+// GetBucketOwnershipControls calls GetBucketOwnershipControlsFunc.
+func (mock *BackendMock) GetBucketOwnershipControls(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
+ if mock.GetBucketOwnershipControlsFunc == nil {
+ panic("BackendMock.GetBucketOwnershipControlsFunc: method is nil but Backend.GetBucketOwnershipControls was just called")
+ }
+ callInfo := struct {
+ ContextMoqParam context.Context
+ Bucket string
+ }{
+ ContextMoqParam: contextMoqParam,
+ Bucket: bucket,
+ }
+ mock.lockGetBucketOwnershipControls.Lock()
+ mock.calls.GetBucketOwnershipControls = append(mock.calls.GetBucketOwnershipControls, callInfo)
+ mock.lockGetBucketOwnershipControls.Unlock()
+ return mock.GetBucketOwnershipControlsFunc(contextMoqParam, bucket)
+}
+
+// GetBucketOwnershipControlsCalls gets all the calls that were made to GetBucketOwnershipControls.
+// Check the length with:
+//
+// len(mockedBackend.GetBucketOwnershipControlsCalls())
+func (mock *BackendMock) GetBucketOwnershipControlsCalls() []struct {
+ ContextMoqParam context.Context
+ Bucket string
+} {
+ var calls []struct {
+ ContextMoqParam context.Context
+ Bucket string
+ }
+ mock.lockGetBucketOwnershipControls.RLock()
+ calls = mock.calls.GetBucketOwnershipControls
+ mock.lockGetBucketOwnershipControls.RUnlock()
+ return calls
+}
+
// GetBucketPolicy calls GetBucketPolicyFunc.
func (mock *BackendMock) GetBucketPolicy(contextMoqParam context.Context, bucket string) ([]byte, error) {
if mock.GetBucketPolicyFunc == nil {
@@ -1971,6 +2088,46 @@ func (mock *BackendMock) PutBucketAclCalls() []struct {
return calls
}
+// PutBucketOwnershipControls calls PutBucketOwnershipControlsFunc.
+func (mock *BackendMock) PutBucketOwnershipControls(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error {
+ if mock.PutBucketOwnershipControlsFunc == nil {
+ panic("BackendMock.PutBucketOwnershipControlsFunc: method is nil but Backend.PutBucketOwnershipControls was just called")
+ }
+ callInfo := struct {
+ ContextMoqParam context.Context
+ Bucket string
+ Ownership types.ObjectOwnership
+ }{
+ ContextMoqParam: contextMoqParam,
+ Bucket: bucket,
+ Ownership: ownership,
+ }
+ mock.lockPutBucketOwnershipControls.Lock()
+ mock.calls.PutBucketOwnershipControls = append(mock.calls.PutBucketOwnershipControls, callInfo)
+ mock.lockPutBucketOwnershipControls.Unlock()
+ return mock.PutBucketOwnershipControlsFunc(contextMoqParam, bucket, ownership)
+}
+
+// PutBucketOwnershipControlsCalls gets all the calls that were made to PutBucketOwnershipControls.
+// Check the length with:
+//
+// len(mockedBackend.PutBucketOwnershipControlsCalls())
+func (mock *BackendMock) PutBucketOwnershipControlsCalls() []struct {
+ ContextMoqParam context.Context
+ Bucket string
+ Ownership types.ObjectOwnership
+} {
+ var calls []struct {
+ ContextMoqParam context.Context
+ Bucket string
+ Ownership types.ObjectOwnership
+ }
+ mock.lockPutBucketOwnershipControls.RLock()
+ calls = mock.calls.PutBucketOwnershipControls
+ mock.lockPutBucketOwnershipControls.RUnlock()
+ return calls
+}
+
// PutBucketPolicy calls PutBucketPolicyFunc.
func (mock *BackendMock) PutBucketPolicy(contextMoqParam context.Context, bucket string, policy []byte) error {
if mock.PutBucketPolicyFunc == nil {
diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go
index e4bea92..ce1475e 100644
--- a/s3api/controllers/base.go
+++ b/s3api/controllers/base.go
@@ -566,6 +566,43 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error {
})
}
+ if ctx.Request().URI().QueryArgs().Has("ownershipControls") {
+ err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
+ Readonly: c.readonly,
+ Acl: parsedAcl,
+ AclPermission: types.PermissionRead,
+ IsRoot: isRoot,
+ Acc: acct,
+ Bucket: bucket,
+ Action: auth.GetBucketOwnershipControlsAction,
+ })
+ if err != nil {
+ return SendXMLResponse(ctx, nil, err,
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionGetBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
+ data, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
+ return SendXMLResponse(ctx,
+ s3response.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: data,
+ },
+ },
+ }, err,
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionGetBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
if ctx.Request().URI().QueryArgs().Has("versioning") {
err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
Readonly: c.readonly,
@@ -933,6 +970,9 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp")
granWrite := ctx.Get("X-Amz-Grant-Write")
grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp")
+ objectOwnership := types.ObjectOwnership(
+ ctx.Get("X-Amz-Object-Ownership", string(types.ObjectOwnershipBucketOwnerEnforced)),
+ )
mfa := ctx.Get("X-Amz-Mfa")
contentMD5 := ctx.Get("Content-MD5")
acct := ctx.Locals("account").(auth.Account)
@@ -1000,6 +1040,57 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
})
}
+ if ctx.Request().URI().QueryArgs().Has("ownershipControls") {
+ parsedAcl := ctx.Locals("parsedAcl").(auth.ACL)
+ var ownershipControls s3response.OwnershipControls
+ if err := xml.Unmarshal(ctx.Body(), &ownershipControls); err != nil {
+ return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionPutBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
+ if len(ownershipControls.Rules) != 1 || !utils.IsValidOwnership(ownershipControls.Rules[0].ObjectOwnership) {
+ return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionPutBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
+ if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
+ Readonly: c.readonly,
+ Acl: parsedAcl,
+ AclPermission: types.PermissionWrite,
+ IsRoot: isRoot,
+ Acc: acct,
+ Bucket: bucket,
+ Action: auth.PutBucketOwnershipControlsAction,
+ }); err != nil {
+ return SendResponse(ctx, err,
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionPutBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
+ err := c.be.PutBucketOwnershipControls(ctx.Context(), bucket, ownershipControls.Rules[0].ObjectOwnership)
+ return SendResponse(ctx, err,
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionPutBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
if ctx.Request().URI().QueryArgs().Has("versioning") {
parsedAcl := ctx.Locals("parsedAcl").(auth.ACL)
err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
@@ -1141,10 +1232,33 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP
if ctx.Request().URI().QueryArgs().Has("acl") {
+ parsedAcl := ctx.Locals("parsedAcl").(auth.ACL)
var input *s3.PutBucketAclInput
- parsedAcl := ctx.Locals("parsedAcl").(auth.ACL)
- err := auth.VerifyAccess(ctx.Context(), c.be,
+ ownership, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
+ if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)) {
+ return SendResponse(ctx, err,
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionPutBucketAcl,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+ if ownership == types.ObjectOwnershipBucketOwnerEnforced {
+ if c.debug {
+ log.Println("bucket acls are disabled")
+ }
+ return SendResponse(ctx, s3err.GetAPIError(s3err.ErrAclNotSupported),
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionPutBucketAcl,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
+ err = auth.VerifyAccess(ctx.Context(), c.be,
auth.AccessOptions{
Readonly: c.readonly,
Acl: parsedAcl,
@@ -1259,7 +1373,6 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
}
}
- fmt.Println(*input, parsedAcl)
updAcl, err := auth.UpdateACL(input, parsedAcl, c.iam)
if err != nil {
return SendResponse(ctx, err,
@@ -1289,16 +1402,45 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
Action: metrics.ActionCreateBucket,
})
}
+ if ok := utils.IsValidOwnership(objectOwnership); !ok {
+ if c.debug {
+ log.Printf("invalid bucket object ownership: %v", objectOwnership)
+ }
+ return SendResponse(ctx, s3err.APIError{
+ Code: "InvalidArgument",
+ Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %v", objectOwnership),
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionCreateBucket,
+ BucketOwner: acct.Access,
+ })
+ }
+
+ if acl+grants != "" && objectOwnership == types.ObjectOwnershipBucketOwnerEnforced {
+ if c.debug {
+ log.Printf("bucket acls are disabled for %v object ownership", objectOwnership)
+ }
+ return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidBucketAclWithObjectOwnership),
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionCreateBucket,
+ BucketOwner: acct.Access,
+ })
+ }
if acl != "" && grants != "" {
if c.debug {
log.Printf("invalid request: %q (grants) %q (acl)", grants, acl)
}
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest),
+ return SendResponse(ctx, s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
&MetaOpts{
Logger: c.logger,
MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
+ Action: metrics.ActionCreateBucket,
BucketOwner: acct.Access,
})
}
@@ -1334,7 +1476,7 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
err = c.be.CreateBucket(ctx.Context(), &s3.CreateBucketInput{
Bucket: &bucket,
- ObjectOwnership: types.ObjectOwnership(acct.Access),
+ ObjectOwnership: objectOwnership,
ObjectLockEnabledForBucket: &lockEnabled,
}, updAcl)
return SendResponse(ctx, err,
@@ -2042,6 +2184,38 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error {
})
}
+ if ctx.Request().URI().QueryArgs().Has("ownershipControls") {
+ err := auth.VerifyAccess(ctx.Context(), c.be,
+ auth.AccessOptions{
+ Readonly: c.readonly,
+ Acl: parsedAcl,
+ AclPermission: types.PermissionWrite,
+ IsRoot: isRoot,
+ Acc: acct,
+ Bucket: bucket,
+ Action: auth.PutBucketOwnershipControlsAction,
+ })
+ if err != nil {
+ return SendResponse(ctx, err,
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionDeleteBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ })
+ }
+
+ err = c.be.DeleteBucketOwnershipControls(ctx.Context(), bucket)
+ return SendResponse(ctx, err,
+ &MetaOpts{
+ Logger: c.logger,
+ MetricsMng: c.mm,
+ Action: metrics.ActionDeleteBucketOwnershipControls,
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ })
+ }
+
if ctx.Request().URI().QueryArgs().Has("policy") {
err := auth.VerifyAccess(ctx.Context(), c.be,
auth.AccessOptions{
diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go
index 74a994f..4aa6ff8 100644
--- a/s3api/controllers/base_test.go
+++ b/s3api/controllers/base_test.go
@@ -395,6 +395,9 @@ func TestS3ApiController_ListActions(t *testing.T) {
GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return objectLockResult, nil
},
+ GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
+ return types.ObjectOwnershipBucketOwnerEnforced, nil
+ },
},
}
@@ -448,6 +451,15 @@ func TestS3ApiController_ListActions(t *testing.T) {
wantErr: false,
statusCode: 404,
},
+ {
+ name: "Get-bucket-ownership-control-success",
+ app: app,
+ args: args{
+ req: httptest.NewRequest(http.MethodGet, "/my-bucket?ownershipControls", nil),
+ },
+ wantErr: false,
+ statusCode: 200,
+ },
{
name: "Get-bucket-tagging-success",
app: app,
@@ -562,7 +574,7 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
app := fiber.New()
// Mock valid acl
- acl := auth.ACL{Owner: "valid access", ACL: "public-read-write"}
+ acl := auth.ACL{Owner: "valid access"}
acldata, err := json.Marshal(acl)
if err != nil {
t.Errorf("Failed to parse the params: %v", err.Error())
@@ -636,6 +648,22 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
`
+ ownershipBody := `
+
+
+ BucketOwnerEnforced
+
+
+ `
+
+ invalidOwnershipBody := `
+
+
+ invalid_value
+
+
+ `
+
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
@@ -659,6 +687,12 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string, config []byte) error {
return nil
},
+ PutBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error {
+ return nil
+ },
+ GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
+ return types.ObjectOwnershipBucketOwnerPreferred, nil
+ },
},
}
// Mock ctx.Locals
@@ -691,6 +725,9 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
errAclBodyReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(body))
errAclBodyReq.Header.Set("X-Amz-Grant-Read", "hello")
+ invAclOwnershipReq := httptest.NewRequest(http.MethodPut, "/my-bucket", nil)
+ invAclOwnershipReq.Header.Set("X-Amz-Grant-Read", "hello")
+
tests := []struct {
name string
app *fiber.App
@@ -716,6 +753,24 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
wantErr: false,
statusCode: 200,
},
+ {
+ name: "Put-bucket-ownership-controls-invalid-ownership",
+ app: app,
+ args: args{
+ req: httptest.NewRequest(http.MethodPut, "/my-bucket?ownershipControls", strings.NewReader(invalidOwnershipBody)),
+ },
+ wantErr: false,
+ statusCode: 400,
+ },
+ {
+ name: "Put-bucket-ownership-controls-success",
+ app: app,
+ args: args{
+ req: httptest.NewRequest(http.MethodPut, "/my-bucket?ownershipControls", strings.NewReader(ownershipBody)),
+ },
+ wantErr: false,
+ statusCode: 200,
+ },
{
name: "Put-object-lock-configuration-invalid-body",
app: app,
@@ -816,7 +871,16 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
statusCode: 200,
},
{
- name: "Put-bucket-invalid-bucket-name",
+ name: "Create-bucket-invalid-acl-ownership-combination",
+ app: app,
+ args: args{
+ req: invAclOwnershipReq,
+ },
+ wantErr: false,
+ statusCode: 400,
+ },
+ {
+ name: "Create-bucket-invalid-bucket-name",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPut, "/aa", nil),
@@ -825,7 +889,7 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
statusCode: 400,
},
{
- name: "Put-bucket-success",
+ name: "Create-bucket-success",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPut, "/my-bucket", nil),
@@ -1160,6 +1224,12 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
DeleteBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) error {
return nil
},
+ DeleteBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) error {
+ return nil
+ },
+ DeleteBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) error {
+ return nil
+ },
},
}
@@ -1198,6 +1268,23 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
wantErr: false,
statusCode: 204,
},
+ {
+ name: "Delete-bucket-ownership-controls-success",
+ app: app,
+ args: args{
+ req: httptest.NewRequest(http.MethodDelete, "/my-bucket?ownershipControls", nil),
+ },
+ wantErr: false,
+ statusCode: 204,
+ }, {
+ name: "Delete-bucket-policy-success",
+ app: app,
+ args: args{
+ req: httptest.NewRequest(http.MethodDelete, "/my-bucket?policy", nil),
+ },
+ wantErr: false,
+ statusCode: 204,
+ },
}
for _, tt := range tests {
resp, err := tt.app.Test(tt.args.req)
diff --git a/s3api/middlewares/acl-parser.go b/s3api/middlewares/acl-parser.go
index 6beb312..593d54c 100644
--- a/s3api/middlewares/acl-parser.go
+++ b/s3api/middlewares/acl-parser.go
@@ -50,7 +50,8 @@ func AclParser(be backend.Backend, logger s3log.AuditLogger, readonly bool) fibe
!ctx.Request().URI().QueryArgs().Has("tagging") &&
!ctx.Request().URI().QueryArgs().Has("versioning") &&
!ctx.Request().URI().QueryArgs().Has("policy") &&
- !ctx.Request().URI().QueryArgs().Has("object-lock") {
+ !ctx.Request().URI().QueryArgs().Has("object-lock") &&
+ !ctx.Request().URI().QueryArgs().Has("ownershipControls") {
if err := auth.MayCreateBucket(acct, isRoot); err != nil {
return controllers.SendXMLResponse(ctx, nil, err, &controllers.MetaOpts{Logger: logger, Action: "CreateBucket"})
}
diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go
index 1c6a22a..38b68a4 100644
--- a/s3api/utils/utils.go
+++ b/s3api/utils/utils.go
@@ -326,3 +326,16 @@ func ParsObjectLockHdrs(ctx *fiber.Ctx) (*objLockCfg, error) {
LegalHoldStatus: legalHold,
}, nil
}
+
+func IsValidOwnership(val types.ObjectOwnership) bool {
+ switch val {
+ case types.ObjectOwnershipBucketOwnerEnforced:
+ return true
+ case types.ObjectOwnershipBucketOwnerPreferred:
+ return true
+ case types.ObjectOwnershipObjectWriter:
+ return true
+ default:
+ return false
+ }
+}
diff --git a/s3api/utils/utils_test.go b/s3api/utils/utils_test.go
index a498745..a359e95 100644
--- a/s3api/utils/utils_test.go
+++ b/s3api/utils/utils_test.go
@@ -335,3 +335,50 @@ func TestFilterObjectAttributes(t *testing.T) {
})
}
}
+
+func TestIsValidOwnership(t *testing.T) {
+ type args struct {
+ val types.ObjectOwnership
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {
+ name: "valid-BucketOwnerEnforced",
+ args: args{
+ val: types.ObjectOwnershipBucketOwnerEnforced,
+ },
+ want: true,
+ },
+ {
+ name: "valid-BucketOwnerPreferred",
+ args: args{
+ val: types.ObjectOwnershipBucketOwnerPreferred,
+ },
+ want: true,
+ },
+ {
+ name: "valid-ObjectWriter",
+ args: args{
+ val: types.ObjectOwnershipObjectWriter,
+ },
+ want: true,
+ },
+ {
+ name: "invalid_value",
+ args: args{
+ val: types.ObjectOwnership("invalid_value"),
+ },
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := IsValidOwnership(tt.args.val); got != tt.want {
+ t.Errorf("IsValidOwnership() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/s3err/s3err.go b/s3err/s3err.go
index cf88293..89ba942 100644
--- a/s3err/s3err.go
+++ b/s3err/s3err.go
@@ -123,6 +123,10 @@ const (
ErrBucketTaggingNotFound
ErrObjectLockInvalidHeaders
ErrRequestTimeTooSkewed
+ ErrInvalidBucketAclWithObjectOwnership
+ ErrBothCannedAndHeaderGrants
+ ErrOwnershipControlsNotFound
+ ErrAclNotSupported
// Non-AWS errors
ErrExistingObjectIsDirectory
@@ -472,6 +476,26 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The difference between the request time and the server's time is too large.",
HTTPStatusCode: http.StatusForbidden,
},
+ ErrInvalidBucketAclWithObjectOwnership: {
+ Code: "ErrInvalidBucketAclWithObjectOwnership",
+ Description: "Bucket cannot have ACLs set with ObjectOwnership's BucketOwnerEnforced setting",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBothCannedAndHeaderGrants: {
+ Code: "InvalidRequest",
+ Description: "Specifying both Canned ACLs and Header Grants is not allowed",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrOwnershipControlsNotFound: {
+ Code: "OwnershipControlsNotFoundError",
+ Description: "The bucket ownership controls were not found",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrAclNotSupported: {
+ Code: "AccessControlListNotSupported",
+ Description: "The bucket does not allow ACLs",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
// non aws errors
ErrExistingObjectIsDirectory: {
diff --git a/s3response/s3response.go b/s3response/s3response.go
index 4540133..0a76766 100644
--- a/s3response/s3response.go
+++ b/s3response/s3response.go
@@ -217,3 +217,7 @@ type Grantee struct {
ID string
DisplayName string
}
+
+type OwnershipControls struct {
+ Rules []types.OwnershipControlsRule `xml:"Rule"`
+}
diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go
index d12aa49..f5166af 100644
--- a/tests/integration/group-tests.go
+++ b/tests/integration/group-tests.go
@@ -65,6 +65,8 @@ func TestCreateBucket(s *S3Conf) {
CreateBucket_invalid_bucket_name(s)
CreateBucket_existing_bucket(s)
CreateBucket_owned_by_you(s)
+ CreateBucket_invalid_ownership(s)
+ CreateBucket_ownership_with_acl(s)
CreateBucket_as_user(s)
CreateBucket_default_acl(s)
CreateBucket_non_default_acl(s)
@@ -89,6 +91,24 @@ func TestDeleteBucket(s *S3Conf) {
DeleteBucket_success_status_code(s)
}
+func TestPutBucketOwnershipControls(s *S3Conf) {
+ PutBucketOwnershipControls_non_existing_bucket(s)
+ PutBucketOwnershipControls_multiple_rules(s)
+ PutBucketOwnershipControls_invalid_ownership(s)
+ PutBucketOwnershipControls_success(s)
+}
+
+func TestGetBucketOwnershipControls(s *S3Conf) {
+ GetBucketOwnershipControls_non_existing_bucket(s)
+ GetBucketOwnershipControls_default_ownership(s)
+ GetBucketOwnershipControls_success(s)
+}
+
+func TestDeleteBucketOwnershipControls(s *S3Conf) {
+ DeleteBucketOwnershipControls_non_existing_bucket(s)
+ DeleteBucketOwnershipControls_success(s)
+}
+
func TestPutBucketTagging(s *S3Conf) {
PutBucketTagging_non_existing_bucket(s)
PutBucketTagging_long_tags(s)
@@ -267,6 +287,7 @@ func TestCompleteMultipartUpload(s *S3Conf) {
func TestPutBucketAcl(s *S3Conf) {
PutBucketAcl_non_existing_bucket(s)
+ PutBucketAcl_disabled(s)
PutBucketAcl_invalid_acl_canned_and_acp(s)
PutBucketAcl_invalid_acl_canned_and_grants(s)
PutBucketAcl_invalid_acl_acp_and_grants(s)
@@ -280,6 +301,9 @@ func TestPutBucketAcl(s *S3Conf) {
func TestGetBucketAcl(s *S3Conf) {
GetBucketAcl_non_existing_bucket(s)
+ GetBucketAcl_translation_canned_public_read(s)
+ GetBucketAcl_translation_canned_public_read_write(s)
+ GetBucketAcl_translation_canned_private(s)
GetBucketAcl_access_denied(s)
GetBucketAcl_success(s)
}
@@ -395,6 +419,9 @@ func TestFullFlow(s *S3Conf) {
TestHeadBucket(s)
TestListBuckets(s)
TestDeleteBucket(s)
+ TestPutBucketOwnershipControls(s)
+ TestGetBucketOwnershipControls(s)
+ TestDeleteBucketOwnershipControls(s)
TestPutBucketTagging(s)
TestGetBucketTagging(s)
TestDeleteBucketTagging(s)
@@ -505,6 +532,8 @@ func GetIntTests() IntTests {
"CreateBucket_invalid_bucket_name": CreateBucket_invalid_bucket_name,
"CreateBucket_existing_bucket": CreateBucket_existing_bucket,
"CreateBucket_owned_by_you": CreateBucket_owned_by_you,
+ "CreateBucket_invalid_ownership": CreateBucket_invalid_ownership,
+ "CreateBucket_ownership_with_acl": CreateBucket_ownership_with_acl,
"CreateBucket_as_user": CreateBucket_as_user,
"CreateDeleteBucket_success": CreateDeleteBucket_success,
"CreateBucket_default_acl": CreateBucket_default_acl,
@@ -518,6 +547,15 @@ func GetIntTests() IntTests {
"DeleteBucket_non_existing_bucket": DeleteBucket_non_existing_bucket,
"DeleteBucket_non_empty_bucket": DeleteBucket_non_empty_bucket,
"DeleteBucket_success_status_code": DeleteBucket_success_status_code,
+ "PutBucketOwnershipControls_non_existing_bucket": PutBucketOwnershipControls_non_existing_bucket,
+ "PutBucketOwnershipControls_multiple_rules": PutBucketOwnershipControls_multiple_rules,
+ "PutBucketOwnershipControls_invalid_ownership": PutBucketOwnershipControls_invalid_ownership,
+ "PutBucketOwnershipControls_success": PutBucketOwnershipControls_success,
+ "GetBucketOwnershipControls_non_existing_bucket": GetBucketOwnershipControls_non_existing_bucket,
+ "GetBucketOwnershipControls_default_ownership": GetBucketOwnershipControls_default_ownership,
+ "GetBucketOwnershipControls_success": GetBucketOwnershipControls_success,
+ "DeleteBucketOwnershipControls_non_existing_bucket": DeleteBucketOwnershipControls_non_existing_bucket,
+ "DeleteBucketOwnershipControls_success": DeleteBucketOwnershipControls_success,
"PutBucketTagging_non_existing_bucket": PutBucketTagging_non_existing_bucket,
"PutBucketTagging_long_tags": PutBucketTagging_long_tags,
"PutBucketTagging_success": PutBucketTagging_success,
@@ -636,6 +674,9 @@ func GetIntTests() IntTests {
"PutBucketAcl_success_canned_acl": PutBucketAcl_success_canned_acl,
"PutBucketAcl_success_acp": PutBucketAcl_success_acp,
"GetBucketAcl_non_existing_bucket": GetBucketAcl_non_existing_bucket,
+ "GetBucketAcl_translation_canned_public_read": GetBucketAcl_translation_canned_public_read,
+ "GetBucketAcl_translation_canned_public_read_write": GetBucketAcl_translation_canned_public_read_write,
+ "GetBucketAcl_translation_canned_private": GetBucketAcl_translation_canned_private,
"GetBucketAcl_access_denied": GetBucketAcl_access_denied,
"GetBucketAcl_success": GetBucketAcl_success,
"PutBucketPolicy_non_existing_bucket": PutBucketPolicy_non_existing_bucket,
diff --git a/tests/integration/tests.go b/tests/integration/tests.go
index 2864973..719f9fe 100644
--- a/tests/integration/tests.go
+++ b/tests/integration/tests.go
@@ -1720,47 +1720,76 @@ func CreateBucket_owned_by_you(s *S3Conf) error {
})
}
-func CreateBucket_default_acl(s *S3Conf) error {
- testName := "CreateBucket_default_acl"
+func CreateBucket_invalid_ownership(s *S3Conf) error {
+ testName := "CreateBucket_invalid_ownership"
runF(testName)
- bucket := getBucketName()
- client := s3.NewFromConfig(s.Config())
-
- err := setup(s, bucket)
- if err != nil {
+ invalidOwnership := types.ObjectOwnership("invalid_ownership")
+ err := setup(s, getBucketName(), withOwnership(invalidOwnership))
+ if err := checkApiErr(err, s3err.APIError{
+ Code: "InvalidArgument",
+ Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %v", invalidOwnership),
+ HTTPStatusCode: http.StatusBadRequest,
+ }); 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 *out.Owner.ID != s.awsID {
- failF("%v: expected bucket owner to be %v, instead got %v", testName, s.awsID, *out.Owner.ID)
- return fmt.Errorf("%v: expected bucket owner to be %v, instead got %v", testName, s.awsID, *out.Owner.ID)
- }
-
- if len(out.Grants) != 0 {
- failF("%v: expected grants to be empty instead got %v", testName, len(out.Grants))
- return fmt.Errorf("%v: expected grants to be empty instead got %v", testName, len(out.Grants))
- }
-
- err = teardown(s, bucket)
- if err != nil {
- failF("%v: %v", 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 := s3.NewFromConfig(s.Config())
+
+ 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 *out.Owner.ID != s.awsID {
+ return fmt.Errorf("expected bucket owner to be %v, instead got %v", s.awsID, *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 *grt.Grantee.ID != s.awsID {
+ return fmt.Errorf("expected the grantee id to be %v, instead got %v", s.awsID, *grt.Grantee.ID)
+ }
+
+ return nil
+ })
+}
+
func CreateBucket_non_default_acl(s *S3Conf) error {
testName := "CreateBucket_non_default_acl"
runF(testName)
@@ -1776,6 +1805,12 @@ func CreateBucket_non_default_acl(s *S3Conf) error {
}
grants := []types.Grant{
+ {
+ Grantee: &types.Grantee{
+ ID: &s.awsID,
+ },
+ Permission: types.PermissionFullControl,
+ },
{
Grantee: &types.Grantee{
ID: getPtr("grt1"),
@@ -1800,7 +1835,13 @@ func CreateBucket_non_default_acl(s *S3Conf) error {
client := s3.NewFromConfig(s.Config())
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- _, err = client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: &bucket, GrantFullControl: getPtr("grt1"), GrantReadACP: getPtr("grt2"), GrantWrite: getPtr("grt3")})
+ _, err = client.CreateBucket(ctx, &s3.CreateBucketInput{
+ Bucket: &bucket,
+ GrantFullControl: getPtr("grt1"),
+ GrantReadACP: getPtr("grt2"),
+ GrantWrite: getPtr("grt3"),
+ ObjectOwnership: types.ObjectOwnershipBucketOwnerPreferred,
+ })
cancel()
if err != nil {
failF("%v: %v", err)
@@ -2195,6 +2236,220 @@ func DeleteBucket_success_status_code(s *S3Conf) error {
return nil
}
+func PutBucketOwnershipControls_non_existing_bucket(s *S3Conf) error {
+ testName := "PutBucketOwnershipControls_non_existing_bucket"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{
+ Bucket: getPtr(getBucketName()),
+ OwnershipControls: &types.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: types.ObjectOwnershipBucketOwnerPreferred,
+ },
+ },
+ },
+ })
+ cancel()
+ if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func PutBucketOwnershipControls_multiple_rules(s *S3Conf) error {
+ testName := "PutBucketOwnershipControls_multiple_rules"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ OwnershipControls: &types.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: types.ObjectOwnershipBucketOwnerPreferred,
+ },
+ {
+ ObjectOwnership: types.ObjectOwnershipObjectWriter,
+ },
+ },
+ },
+ })
+ cancel()
+ if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func PutBucketOwnershipControls_invalid_ownership(s *S3Conf) error {
+ testName := "PutBucketOwnershipControls_invalid_ownership"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ OwnershipControls: &types.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: types.ObjectOwnership("invalid_ownership"),
+ },
+ },
+ },
+ })
+ cancel()
+ if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func PutBucketOwnershipControls_success(s *S3Conf) error {
+ testName := "PutBucketOwnershipControls_success"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ OwnershipControls: &types.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: types.ObjectOwnershipObjectWriter,
+ },
+ },
+ },
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func GetBucketOwnershipControls_non_existing_bucket(s *S3Conf) error {
+ testName := "GetBucketOwnershipControls_non_existing_bucket"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
+ Bucket: getPtr(getBucketName()),
+ })
+ cancel()
+ if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func GetBucketOwnershipControls_default_ownership(s *S3Conf) error {
+ testName := "GetBucketOwnershipControls_default_ownership"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ resp, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ if len(resp.OwnershipControls.Rules) != 1 {
+ return fmt.Errorf("expected ownership control rules length to be 1, instead got %v", len(resp.OwnershipControls.Rules))
+ }
+ if resp.OwnershipControls.Rules[0].ObjectOwnership != types.ObjectOwnershipBucketOwnerEnforced {
+ return fmt.Errorf("expected the bucket ownership to be %v, instead got %v", types.ObjectOwnershipBucketOwnerEnforced, resp.OwnershipControls.Rules[0].ObjectOwnership)
+ }
+
+ return nil
+ })
+}
+
+func GetBucketOwnershipControls_success(s *S3Conf) error {
+ testName := "GetBucketOwnershipControls_success"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ OwnershipControls: &types.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: types.ObjectOwnershipObjectWriter,
+ },
+ },
+ },
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
+ resp, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ if len(resp.OwnershipControls.Rules) != 1 {
+ return fmt.Errorf("expected ownership control rules length to be 1, instead got %v", len(resp.OwnershipControls.Rules))
+ }
+ if resp.OwnershipControls.Rules[0].ObjectOwnership != types.ObjectOwnershipObjectWriter {
+ return fmt.Errorf("expected the bucket ownership to be %v, instead got %v", types.ObjectOwnershipObjectWriter, resp.OwnershipControls.Rules[0].ObjectOwnership)
+ }
+
+ return nil
+ })
+}
+
+func DeleteBucketOwnershipControls_non_existing_bucket(s *S3Conf) error {
+ testName := "DeleteBucketOwnershipControls_non_existing_bucket"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.DeleteBucketOwnershipControls(ctx, &s3.DeleteBucketOwnershipControlsInput{
+ Bucket: getPtr(getBucketName()),
+ })
+ cancel()
+ if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func DeleteBucketOwnershipControls_success(s *S3Conf) error {
+ testName := "DeleteBucketOwnershipControls_success"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.DeleteBucketOwnershipControls(ctx, &s3.DeleteBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
+ _, err = s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
func PutBucketTagging_non_existing_bucket(s *S3Conf) error {
testName := "PutBucketTagging_non_existing_bucket"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
@@ -5893,6 +6148,23 @@ func PutBucketAcl_non_existing_bucket(s *S3Conf) error {
})
}
+func PutBucketAcl_disabled(s *S3Conf) error {
+ testName := "PutBucketAcl_disabled"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
+ Bucket: &bucket,
+ ACL: types.BucketCannedACLPublicRead,
+ GrantRead: &s.awsID,
+ })
+ cancel()
+ if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAclNotSupported)); err != nil {
+ return err
+ }
+ return nil
+ })
+}
+
func PutBucketAcl_invalid_acl_canned_and_acp(s *S3Conf) error {
testName := "PutBucketAcl_invalid_acl_canned_and_acp"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
@@ -5908,7 +6180,7 @@ func PutBucketAcl_invalid_acl_canned_and_acp(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_invalid_acl_canned_and_grants(s *S3Conf) error {
@@ -5938,7 +6210,7 @@ func PutBucketAcl_invalid_acl_canned_and_grants(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_invalid_acl_acp_and_grants(s *S3Conf) error {
@@ -5968,7 +6240,7 @@ func PutBucketAcl_invalid_acl_acp_and_grants(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_invalid_owner(s *S3Conf) error {
@@ -5997,7 +6269,7 @@ func PutBucketAcl_invalid_owner(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_invalid_owner_not_in_body(s *S3Conf) error {
@@ -6023,7 +6295,7 @@ func PutBucketAcl_invalid_owner_not_in_body(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_success_access_denied(s *S3Conf) error {
@@ -6068,7 +6340,7 @@ func PutBucketAcl_success_access_denied(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_success_canned_acl(s *S3Conf) error {
@@ -6100,7 +6372,7 @@ func PutBucketAcl_success_canned_acl(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_success_acp(s *S3Conf) error {
@@ -6141,7 +6413,7 @@ func PutBucketAcl_success_acp(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketAcl_success_grants(s *S3Conf) error {
@@ -6186,7 +6458,7 @@ func PutBucketAcl_success_grants(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func GetBucketAcl_non_existing_bucket(s *S3Conf) error {
@@ -6205,6 +6477,156 @@ func GetBucketAcl_non_existing_bucket(s *S3Conf) error {
})
}
+func GetBucketAcl_translation_canned_public_read(s *S3Conf) error {
+ testName := "GetBucketAcl_translation_canned_public_read"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ grants := []types.Grant{
+ {
+ Grantee: &types.Grantee{
+ ID: &s.awsID,
+ Type: types.TypeCanonicalUser,
+ },
+ Permission: types.PermissionFullControl,
+ },
+ {
+ Grantee: &types.Grantee{
+ ID: getPtr("all-users"),
+ Type: types.TypeCanonicalUser,
+ },
+ Permission: types.PermissionRead,
+ },
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
+ Bucket: &bucket,
+ ACL: types.BucketCannedACLPublicRead,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
+ out, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ if ok := compareGrants(out.Grants, grants); !ok {
+ return fmt.Errorf("expected grants to be %v, instead got %v", grants, out.Grants)
+ }
+ if *out.Owner.ID != s.awsID {
+ return fmt.Errorf("expected bucket owner to be %v, instead got %v", s.awsID, *out.Owner.ID)
+ }
+
+ return nil
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
+}
+
+func GetBucketAcl_translation_canned_public_read_write(s *S3Conf) error {
+ testName := "GetBucketAcl_translation_canned_public_read_write"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ grants := []types.Grant{
+ {
+ Grantee: &types.Grantee{
+ ID: &s.awsID,
+ Type: types.TypeCanonicalUser,
+ },
+ Permission: types.PermissionFullControl,
+ },
+ {
+ Grantee: &types.Grantee{
+ ID: getPtr("all-users"),
+ Type: types.TypeCanonicalUser,
+ },
+ Permission: types.PermissionRead,
+ },
+ {
+ Grantee: &types.Grantee{
+ ID: getPtr("all-users"),
+ Type: types.TypeCanonicalUser,
+ },
+ Permission: types.PermissionWrite,
+ },
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
+ Bucket: &bucket,
+ ACL: types.BucketCannedACLPublicReadWrite,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
+ out, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ if ok := compareGrants(out.Grants, grants); !ok {
+ return fmt.Errorf("expected grants to be %v, instead got %v", grants, out.Grants)
+ }
+ if *out.Owner.ID != s.awsID {
+ return fmt.Errorf("expected bucket owner to be %v, instead got %v", s.awsID, *out.Owner.ID)
+ }
+
+ return nil
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
+}
+
+func GetBucketAcl_translation_canned_private(s *S3Conf) error {
+ testName := "GetBucketAcl_translation_canned_private"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ grants := []types.Grant{
+ {
+ Grantee: &types.Grantee{
+ ID: &s.awsID,
+ Type: types.TypeCanonicalUser,
+ },
+ Permission: types.PermissionFullControl,
+ },
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
+ Bucket: &bucket,
+ ACL: types.BucketCannedACLPrivate,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
+ out, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ if ok := compareGrants(out.Grants, grants); !ok {
+ return fmt.Errorf("expected grants to be %v, instead got %v", grants, out.Grants)
+ }
+ if *out.Owner.ID != s.awsID {
+ return fmt.Errorf("expected bucket owner to be %v, instead got %v", s.awsID, *out.Owner.ID)
+ }
+
+ return nil
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
+}
+
func GetBucketAcl_access_denied(s *S3Conf) error {
testName := "GetBucketAcl_access_denied"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
@@ -6291,6 +6713,15 @@ func GetBucketAcl_success(s *S3Conf) error {
return err
}
+ grants = append([]types.Grant{
+ {
+ Grantee: &types.Grantee{
+ ID: &s.awsID,
+ },
+ Permission: types.PermissionFullControl,
+ },
+ }, grants...)
+
if ok := compareGrants(out.Grants, grants); !ok {
return fmt.Errorf("expected grants to be %v, instead got %v", grants, out.Grants)
}
@@ -6299,7 +6730,7 @@ func GetBucketAcl_success(s *S3Conf) error {
}
return nil
- })
+ }, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func PutBucketPolicy_non_existing_bucket(s *S3Conf) error {
diff --git a/tests/integration/utils.go b/tests/integration/utils.go
index c3dafef..84de0ea 100644
--- a/tests/integration/utils.go
+++ b/tests/integration/utils.go
@@ -66,6 +66,7 @@ func setup(s *S3Conf, bucket string, opts ...setupOpt) error {
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
ObjectLockEnabledForBucket: &cfg.LockEnabled,
+ ObjectOwnership: cfg.Ownership,
})
cancel()
return err
@@ -121,6 +122,7 @@ func teardown(s *S3Conf, bucket string) error {
type setupCfg struct {
LockEnabled bool
+ Ownership types.ObjectOwnership
}
type setupOpt func(*setupCfg)
@@ -128,6 +130,9 @@ type setupOpt func(*setupCfg)
func withLock() setupOpt {
return func(s *setupCfg) { s.LockEnabled = true }
}
+func withOwnership(o types.ObjectOwnership) setupOpt {
+ return func(s *setupCfg) { s.Ownership = o }
+}
func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error, opts ...setupOpt) error {
runF(testName)