diff --git a/auth/access-control.go b/auth/access-control.go new file mode 100644 index 0000000..d984c9d --- /dev/null +++ b/auth/access-control.go @@ -0,0 +1,201 @@ +// 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" + "errors" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/s3err" +) + +func VerifyObjectCopyAccess(ctx context.Context, be backend.Backend, copySource string, opts AccessOptions) error { + if opts.IsRoot { + return nil + } + if opts.Acc.Role == RoleAdmin { + return nil + } + + // Verify destination bucket access + if err := VerifyAccess(ctx, be, opts); err != nil { + return err + } + // Verify source bucket access + srcBucket, srcObject, found := strings.Cut(copySource, "/") + if !found { + return s3err.GetAPIError(s3err.ErrInvalidCopySource) + } + + // Get source bucket ACL + srcBucketACLBytes, err := be.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &srcBucket}) + if err != nil { + return err + } + + var srcBucketAcl ACL + if err := json.Unmarshal(srcBucketACLBytes, &srcBucketAcl); err != nil { + return err + } + + if err := VerifyAccess(ctx, be, AccessOptions{ + Acl: srcBucketAcl, + AclPermission: PermissionRead, + IsRoot: opts.IsRoot, + Acc: opts.Acc, + Bucket: srcBucket, + Object: srcObject, + Action: GetObjectAction, + }); err != nil { + return err + } + + return nil +} + +type AccessOptions struct { + Acl ACL + AclPermission Permission + IsRoot bool + Acc Account + Bucket string + Object string + Action Action + Readonly bool + IsBucketPublic bool +} + +func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) error { + // Skip the access check for public buckets + if opts.IsBucketPublic { + return nil + } + if opts.Readonly { + if opts.AclPermission == PermissionWrite || opts.AclPermission == PermissionWriteAcp { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + } + if opts.IsRoot { + return nil + } + if opts.Acc.Role == RoleAdmin { + return nil + } + + policy, policyErr := be.GetBucketPolicy(ctx, opts.Bucket) + 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 err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission); err != nil { + return err + } + + return nil +} + +// Detects if the action is policy related +// e.g. +// 'GetBucketPolicy', 'PutBucketPolicy' +func isPolicyAction(action Action) bool { + return action == GetBucketPolicyAction || action == PutBucketPolicyAction +} + +// VerifyPublicAccess checks if the bucket is publically accessible by ACL or Policy +func VerifyPublicAccess(ctx context.Context, be backend.Backend, action Action, permission Permission, bucket, object string) error { + // ACL disabled + policy, err := be.GetBucketPolicy(ctx, bucket) + if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) { + return err + } + if err == nil { + err = VerifyPublicBucketPolicy(policy, bucket, object, action) + if err == nil { + // if ACLs are disabled, and the bucket grants public access, + // policy actions should return 'MethodNotAllowed' + if isPolicyAction(action) { + return s3err.GetAPIError(s3err.ErrMethodNotAllowed) + } + + return nil + } + } + + // if the action is not in the ACL whitelist the access is denied + _, ok := publicACLAllowedActions[action] + if !ok { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + + err = VerifyPublicBucketACL(ctx, be, bucket, action, permission) + if err != nil { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + + return nil +} + +func MayCreateBucket(acct Account, isRoot bool) error { + if isRoot { + return nil + } + + if acct.Role == RoleUser { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + + return nil +} + +func IsAdminOrOwner(acct Account, isRoot bool, acl ACL) error { + // Owner check + if acct.Access == acl.Owner { + return nil + } + + // Root user has access over almost everything + if isRoot { + return nil + } + + // Admin user case + if acct.Role == RoleAdmin { + return nil + } + + // Return access denied in all other cases + return s3err.GetAPIError(s3err.ErrAccessDenied) +} + +type PublicACLAllowedActions map[Action]struct{} + +var publicACLAllowedActions PublicACLAllowedActions = PublicACLAllowedActions{ + ListBucketAction: struct{}{}, + PutObjectAction: struct{}{}, + ListBucketMultipartUploadsAction: struct{}{}, + DeleteObjectAction: struct{}{}, + ListBucketVersionsAction: struct{}{}, + GetObjectAction: struct{}{}, + GetObjectAttributesAction: struct{}{}, + GetObjectAclAction: struct{}{}, +} diff --git a/auth/acl.go b/auth/acl.go index 7842967..3b32cda 100644 --- a/auth/acl.go +++ b/auth/acl.go @@ -33,6 +33,17 @@ type ACL struct { 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 @@ -435,117 +446,22 @@ func verifyACL(acl ACL, access string, permission Permission) error { return s3err.GetAPIError(s3err.ErrAccessDenied) } -func MayCreateBucket(acct Account, isRoot bool) error { - if isRoot { - return nil - } - - if acct.Role == RoleUser { - return s3err.GetAPIError(s3err.ErrAccessDenied) - } - - return nil -} - -func IsAdminOrOwner(acct Account, isRoot bool, acl ACL) error { - // Owner check - if acct.Access == acl.Owner { - return nil - } - - // Root user has access over almost everything - if isRoot { - return nil - } - - // Admin user case - if acct.Role == RoleAdmin { - return nil - } - - // Return access denied in all other cases - return s3err.GetAPIError(s3err.ErrAccessDenied) -} - -type AccessOptions struct { - Acl ACL - AclPermission Permission - IsRoot bool - Acc Account - Bucket string - Object string - Action Action - Readonly bool -} - -func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) error { - if opts.Readonly { - if opts.AclPermission == PermissionWrite || opts.AclPermission == PermissionWriteAcp { - return s3err.GetAPIError(s3err.ErrAccessDenied) - } - } - if opts.IsRoot { - return nil - } - if opts.Acc.Role == RoleAdmin { - return nil - } - - policy, policyErr := be.GetBucketPolicy(ctx, opts.Bucket) - 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 err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission); err != nil { - return err - } - - return nil -} - -func VerifyObjectCopyAccess(ctx context.Context, be backend.Backend, copySource string, opts AccessOptions) error { - if opts.IsRoot { - return nil - } - if opts.Acc.Role == RoleAdmin { - return nil - } - - // Verify destination bucket access - if err := VerifyAccess(ctx, be, opts); err != nil { - return err - } - // Verify source bucket access - srcBucket, srcObject, found := strings.Cut(copySource, "/") - if !found { - return s3err.GetAPIError(s3err.ErrInvalidCopySource) - } - - // Get source bucket ACL - srcBucketACLBytes, err := be.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &srcBucket}) +// Verifies if the bucket acl grants public access +func VerifyPublicBucketACL(ctx context.Context, be backend.Backend, bucket string, action Action, permission Permission) error { + aclBytes, err := be.GetBucketAcl(ctx, &s3.GetBucketAclInput{ + Bucket: &bucket, + }) if err != nil { return err } - var srcBucketAcl ACL - if err := json.Unmarshal(srcBucketACLBytes, &srcBucketAcl); err != nil { + acl, err := ParseACL(aclBytes) + if err != nil { return err } - if err := VerifyAccess(ctx, be, AccessOptions{ - Acl: srcBucketAcl, - AclPermission: PermissionRead, - IsRoot: opts.IsRoot, - Acc: opts.Acc, - Bucket: srcBucket, - Object: srcObject, - Action: GetObjectAction, - }); err != nil { - return err + if !acl.IsPublic(permission) { + return ErrAccessDenied } return nil diff --git a/auth/bucket_policy.go b/auth/bucket_policy.go index 50996b6..4f81719 100644 --- a/auth/bucket_policy.go +++ b/auth/bucket_policy.go @@ -22,6 +22,8 @@ import ( "github.com/versity/versitygw/s3err" ) +var ErrAccessDenied = errors.New("access denied") + type policyErr string func (p policyErr) Error() string { @@ -89,6 +91,24 @@ func (bp *BucketPolicy) isAllowed(principal string, action Action, resource stri return isAllowed } +// isPublic checks if the bucket policy statements contain +// an entity granting public access +func (bp *BucketPolicy) isPublic(resource string, action Action) bool { + var isAllowed bool + for _, statement := range bp.Statement { + if statement.isPublic(resource, action) { + switch statement.Effect { + case BucketPolicyAccessTypeAllow: + isAllowed = true + case BucketPolicyAccessTypeDeny: + return false + } + } + } + + return isAllowed +} + type BucketPolicyItem struct { Effect BucketPolicyAccessType `json:"Effect"` Principals Principals `json:"Principal"` @@ -134,6 +154,11 @@ func (bpi *BucketPolicyItem) findMatch(principal string, action Action, resource return false } +// isPublic checks if the bucket policy statemant grants public access +func (bpi *BucketPolicyItem) isPublic(resource string, action Action) bool { + return bpi.Principals.IsPublic() && bpi.Actions.FindMatch(action) && bpi.Resources.FindMatch(resource) +} + func getMalformedPolicyError(err error) error { return s3err.APIError{ Code: "MalformedPolicy", @@ -183,3 +208,22 @@ func VerifyBucketPolicy(policy []byte, access, bucket, object string, action Act return nil } + +// Checks if the bucket policy grants public access +func VerifyPublicBucketPolicy(policy []byte, bucket, object string, action Action) error { + var bucketPolicy BucketPolicy + if err := json.Unmarshal(policy, &bucketPolicy); err != nil { + return err + } + + resource := bucket + if object != "" { + resource += "/" + object + } + + if !bucketPolicy.isPublic(resource, action) { + return ErrAccessDenied + } + + return nil +} diff --git a/auth/bucket_policy_actions.go b/auth/bucket_policy_actions.go index bab769d..35b5d99 100644 --- a/auth/bucket_policy_actions.go +++ b/auth/bucket_policy_actions.go @@ -91,6 +91,7 @@ var supportedActionList = map[Action]struct{}{ DeleteObjectTaggingAction: {}, ListBucketVersionsAction: {}, ListBucketAction: {}, + GetBucketObjectLockConfigurationAction: {}, PutBucketObjectLockConfigurationAction: {}, GetObjectLegalHoldAction: {}, PutObjectLegalHoldAction: {}, diff --git a/auth/bucket_policy_principals.go b/auth/bucket_policy_principals.go index 828db29..306bb1a 100644 --- a/auth/bucket_policy_principals.go +++ b/auth/bucket_policy_principals.go @@ -121,3 +121,10 @@ func (p Principals) Contains(userAccess string) bool { _, found := p[userAccess] return found } + +// Bucket policy grants public access, if it contains +// a wildcard match to all the users +func (p Principals) IsPublic() bool { + _, ok := p["*"] + return ok +} diff --git a/auth/object_lock.go b/auth/object_lock.go index 042dc67..cb0d064 100644 --- a/auth/object_lock.go +++ b/auth/object_lock.go @@ -136,7 +136,7 @@ func ParseObjectLegalHoldOutput(status *bool) *s3response.GetObjectLegalHoldResu } } -func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []types.ObjectIdentifier, bypass bool, be backend.Backend) error { +func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []types.ObjectIdentifier, bypass, isBucketPublic bool, be backend.Backend) error { data, err := be.GetObjectLockConfiguration(ctx, bucket) if err != nil { if errors.Is(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)) { @@ -211,7 +211,11 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ if err != nil { return err } - err = VerifyBucketPolicy(policy, userAccess, bucket, key, BypassGovernanceRetentionAction) + if isBucketPublic { + err = VerifyPublicBucketPolicy(policy, bucket, key, BypassGovernanceRetentionAction) + } else { + err = VerifyBucketPolicy(policy, userAccess, bucket, key, BypassGovernanceRetentionAction) + } if err != nil { return s3err.GetAPIError(s3err.ErrObjectLocked) } @@ -254,7 +258,11 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ if err != nil { return err } - err = VerifyBucketPolicy(policy, userAccess, bucket, key, BypassGovernanceRetentionAction) + if isBucketPublic { + err = VerifyPublicBucketPolicy(policy, bucket, key, BypassGovernanceRetentionAction) + } else { + err = VerifyBucketPolicy(policy, userAccess, bucket, key, BypassGovernanceRetentionAction) + } if err != nil { return s3err.GetAPIError(s3err.ErrObjectLocked) } diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index c1df2bb..0aa1403 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -77,7 +77,7 @@ func (c S3ApiController) ListBuckets(ctx *fiber.Ctx) error { cToken := ctx.Query("continuation-token") prefix := ctx.Query("prefix") maxBucketsStr := ctx.Query("max-buckets") - acct := ctx.Locals("account").(auth.Account) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) var maxBuckets int32 = 10000 if maxBucketsStr != "" { @@ -119,9 +119,10 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { uploadId := ctx.Query("uploadId") partNumberMarker := ctx.Query("part-number-marker") acceptRange := ctx.Get("Range") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) versionId := ctx.Query("versionId") if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") @@ -133,14 +134,15 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("tagging") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectTaggingAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectTaggingAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -182,14 +184,15 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("retention") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectRetentionAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectRetentionAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -224,14 +227,15 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("legal-hold") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectLegalHoldAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectLegalHoldAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -289,14 +293,15 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { } err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.ListMultipartUploadPartsAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.ListMultipartUploadPartsAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -326,14 +331,15 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("acl") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionReadAcp, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAclAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionReadAcp, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAclAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -359,14 +365,15 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("attributes") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAttributesAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAttributesAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -476,14 +483,15 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { } err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: action, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: action, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendResponse(ctx, err, @@ -509,7 +517,7 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { }) } - ctx.Locals("skip-res-body-log", true) + utils.ContextKeySkipResBodyLog.Set(ctx, true) res, err := c.be.GetObject(ctx.Context(), &s3.GetObjectInput{ Bucket: &bucket, Key: &key, @@ -729,19 +737,21 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { maxUploadsStr := ctx.Query("max-uploads") uploadIdMarker := ctx.Query("upload-id-marker") versionIdMarker := ctx.Query("version-id-marker") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) if ctx.Request().URI().QueryArgs().Has("tagging") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketTaggingAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketTaggingAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -783,13 +793,14 @@ 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: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketOwnershipControlsAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketOwnershipControlsAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -820,13 +831,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("versioning") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketVersioningAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketVersioningAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -860,13 +872,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("policy") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketPolicyAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketPolicyAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -890,13 +903,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("cors") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketCorsAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketCorsAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -920,13 +934,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("versions") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketVersionsAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketVersionsAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -973,13 +988,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("object-lock") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketObjectLockConfigurationAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketObjectLockConfigurationAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -1014,13 +1030,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("acl") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionReadAcp, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketAclAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionReadAcp, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketAclAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -1054,13 +1071,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("uploads") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketMultipartUploadsAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketMultipartUploadsAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -1105,13 +1123,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { if ctx.QueryInt("list-type") == 2 { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -1158,13 +1177,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { } err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -1221,11 +1241,12 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { ) // mfa := ctx.Get("X-Amz-Mfa") // contentMD5 := ctx.Get("Content-MD5") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) if ctx.Request().URI().QueryArgs().Has("tagging") { - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) tagging, err := utils.ParseTagging(ctx.Body(), utils.TagLimitBucket) if err != nil { @@ -1239,13 +1260,14 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { } err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketTaggingAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketTaggingAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendResponse(ctx, err, @@ -1269,7 +1291,7 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { } if ctx.Request().URI().QueryArgs().Has("ownershipControls") { - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) var ownershipControls s3response.OwnershipControls if err := xml.Unmarshal(ctx.Body(), &ownershipControls); err != nil { if c.debug { @@ -1328,15 +1350,16 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { } if ctx.Request().URI().QueryArgs().Has("versioning") { - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketVersioningAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketVersioningAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendResponse(ctx, err, @@ -1389,16 +1412,17 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { } if ctx.Request().URI().QueryArgs().Has("object-lock") { - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketObjectLockConfigurationAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketObjectLockConfigurationAction, + IsBucketPublic: isPublicBucket, }); err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -1431,15 +1455,16 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { } if ctx.Request().URI().QueryArgs().Has("cors") { - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketCorsAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketCorsAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendResponse(ctx, err, @@ -1464,7 +1489,7 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { } } if ctx.Request().URI().QueryArgs().Has("policy") { - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Readonly: c.readonly, Acl: parsedAcl, @@ -1509,7 +1534,7 @@ 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) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) var input *auth.PutBucketAclInput ownership, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket) @@ -1796,15 +1821,16 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") versionId := ctx.Query("versionId") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) contentType := ctx.Get("Content-Type") contentEncoding := ctx.Get("Content-Encoding") contentDisposition := ctx.Get("Content-Disposition") contentLanguage := ctx.Get("Content-Language") cacheControl := ctx.Get("Cache-Control") expires := ctx.Get("Expires") - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) tagging := ctx.Get("x-amz-tagging") // Copy source headers @@ -1866,14 +1892,15 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { } err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: keyStart, - Action: auth.PutBucketTaggingAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutBucketTaggingAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -1899,14 +1926,15 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("retention") { if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: keyStart, - Action: auth.PutObjectRetentionAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutObjectRetentionAction, + IsBucketPublic: IsBucketPublic, }); err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -1981,14 +2009,15 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { } if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: keyStart, - Action: auth.PutObjectLegalHoldAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutObjectLegalHoldAction, + IsBucketPublic: IsBucketPublic, }); err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -2046,13 +2075,14 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { err = auth.VerifyObjectCopyAccess(ctx.Context(), c.be, copySource, auth.AccessOptions{ - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: keyStart, - Action: auth.PutObjectAction, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -2109,14 +2139,15 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: keyStart, - Action: auth.PutObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -2158,7 +2189,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { } var body io.Reader - bodyi := ctx.Locals("body-reader") + bodyi := utils.ContextKeyBodyReader.Get(ctx) if bodyi != nil { body = bodyi.(io.Reader) } else { @@ -2562,14 +2593,15 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: keyStart, - Action: auth.PutObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: keyStart, + Action: auth.PutObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -2581,7 +2613,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &keyStart}}, true, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &keyStart}}, true, IsBucketPublic, c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -2630,7 +2662,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { } var body io.Reader - bodyi := ctx.Locals("body-reader") + bodyi := utils.ContextKeyBodyReader.Get(ctx) if bodyi != nil { body = bodyi.(io.Reader) } else { @@ -2739,20 +2771,22 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error { bucket := ctx.Params("bucket") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) if ctx.Request().URI().QueryArgs().Has("tagging") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketTaggingAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketTaggingAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -2842,13 +2876,14 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("cors") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketCorsAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketCorsAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -2872,13 +2907,14 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.DeleteBucketAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.DeleteBucketAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -2903,9 +2939,10 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error { func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { bucket := ctx.Params("bucket") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) bypassHdr := ctx.Get("X-Amz-Bypass-Governance-Retention") var dObj s3response.DeleteObjects @@ -2925,13 +2962,14 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.DeleteObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.DeleteObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -2946,7 +2984,7 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { // The AWS CLI sends 'True', while Go SDK sends 'true' bypass := strings.EqualFold(bypassHdr, "true") - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, dObj.Objects, bypass, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, dObj.Objects, bypass, IsBucketPublic, c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -2982,9 +3020,10 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") versionId := ctx.Query("versionId") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) bypassHdr := ctx.Get("X-Amz-Bypass-Governance-Retention") if keyEnd != "" { @@ -2998,14 +3037,15 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { if ctx.Request().URI().QueryArgs().Has("tagging") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.DeleteObjectTaggingAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.DeleteObjectTaggingAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -3036,14 +3076,15 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.AbortMultipartUploadAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.AbortMultipartUploadAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -3077,14 +3118,15 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.DeleteObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.DeleteObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -3099,7 +3141,7 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { // The AWS CLI sends 'True', while Go SDK sends 'true' bypass := strings.EqualFold(bypassHdr, "true") - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &key, VersionId: &versionId}}, bypass, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &key, VersionId: &versionId}}, bypass, IsBucketPublic, c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -3159,20 +3201,22 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) error { bucket := ctx.Params("bucket") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - region := ctx.Locals("region").(string) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + region := utils.ContextKeyRegion.Get(ctx).(string) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendResponse(ctx, err, @@ -3214,9 +3258,10 @@ const ( func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error { bucket := ctx.Params("bucket") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) + isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) partNumberQuery := int32(ctx.QueryInt("partNumber", -1)) versionId := ctx.Query("versionId") objRange := ctx.Get("Range") @@ -3250,14 +3295,15 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAction, + IsBucketPublic: isPublicBucket, }) if err != nil { return SendResponse(ctx, err, @@ -3475,9 +3521,10 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { key := ctx.Params("key") keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") - acct := ctx.Locals("account").(auth.Account) - isRoot := ctx.Locals("isRoot").(bool) - parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) + isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) + parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) contentType := ctx.Get("Content-Type") contentDisposition := ctx.Get("Content-Disposition") contentLanguage := ctx.Get("Content-Language") @@ -3512,14 +3559,15 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { } err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.RestoreObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.RestoreObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendResponse(ctx, err, @@ -3567,14 +3615,15 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendXMLResponse(ctx, nil, err, @@ -3673,14 +3722,15 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectAction, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectAction, + IsBucketPublic: IsBucketPublic, }) if err != nil { return SendXMLResponse(ctx, nil, err, diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index 648a297..74d1636 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -32,6 +32,7 @@ import ( "github.com/valyala/fasthttp" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3response" ) @@ -99,8 +100,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) { } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access", Role: "admin:"}) - ctx.Locals("isDebug", false) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access", Role: "admin:"}) return ctx.Next() }) app.Get("/", s3ApiController.ListBuckets) @@ -116,8 +116,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) { } appErr.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access", Role: "admin:"}) - ctx.Locals("isDebug", false) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access", Role: "admin:"}) return ctx.Next() }) appErr.Get("/", s3ApiControllerErr.ListBuckets) @@ -220,10 +219,9 @@ func TestS3ApiController_GetActions(t *testing.T) { }, } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) app.Get("/:bucket/:key/*", s3ApiController.GetActions) @@ -413,10 +411,9 @@ func TestS3ApiController_ListActions(t *testing.T) { } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) @@ -438,10 +435,9 @@ func TestS3ApiController_ListActions(t *testing.T) { } appError := fiber.New() appError.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) appError.Get("/:bucket", s3ApiControllerError.ListActions) @@ -707,10 +703,9 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { } // Mock ctx.Locals app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{Owner: "valid access"}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{Owner: "valid access"}) return ctx.Next() }) app.Put("/:bucket", s3ApiController.PutBucketActions) @@ -1003,10 +998,9 @@ func TestS3ApiController_PutActions(t *testing.T) { }, } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) app.Put("/:bucket/:key/*", s3ApiController.PutActions) @@ -1292,10 +1286,9 @@ func TestS3ApiController_DeleteBucket(t *testing.T) { } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) @@ -1378,10 +1371,9 @@ func TestS3ApiController_DeleteObjects(t *testing.T) { } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) app.Post("/:bucket", s3ApiController.DeleteObjects) @@ -1458,10 +1450,9 @@ func TestS3ApiController_DeleteActions(t *testing.T) { } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions) @@ -1482,10 +1473,9 @@ func TestS3ApiController_DeleteActions(t *testing.T) { }} appErr.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) appErr.Delete("/:bucket/:key/*", s3ApiControllerErr.DeleteActions) @@ -1565,11 +1555,10 @@ func TestS3ApiController_HeadBucket(t *testing.T) { } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) - ctx.Locals("region", "us-east-1") + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) + utils.ContextKeyRegion.Set(ctx, "us-east-1") return ctx.Next() }) @@ -1583,17 +1572,16 @@ func TestS3ApiController_HeadBucket(t *testing.T) { return acldata, nil }, HeadBucketFunc: func(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) { - return nil, s3err.GetAPIError(3) + return nil, s3err.GetAPIError(s3err.ErrBucketNotEmpty) }, }, } appErr.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) - ctx.Locals("region", "us-east-1") + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) + utils.ContextKeyRegion.Set(ctx, "us-east-1") return ctx.Next() }) @@ -1670,10 +1658,9 @@ func TestS3ApiController_HeadObject(t *testing.T) { } app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) app.Head("/:bucket/:key/*", s3ApiController.HeadObject) @@ -1693,10 +1680,9 @@ func TestS3ApiController_HeadObject(t *testing.T) { } appErr.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) appErr.Head("/:bucket/:key/*", s3ApiControllerErr.HeadObject) @@ -1798,10 +1784,9 @@ func TestS3ApiController_CreateActions(t *testing.T) { ` app.Use(func(ctx *fiber.Ctx) error { - ctx.Locals("account", auth.Account{Access: "valid access"}) - ctx.Locals("isRoot", true) - ctx.Locals("isDebug", false) - ctx.Locals("parsedAcl", auth.ACL{}) + utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"}) + utils.ContextKeyIsRoot.Set(ctx, true) + utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{}) return ctx.Next() }) app.Post("/:bucket/:key/*", s3ApiController.CreateActions) diff --git a/s3api/middlewares/acl-parser.go b/s3api/middlewares/acl-parser.go index 61633c3..ba1fc21 100644 --- a/s3api/middlewares/acl-parser.go +++ b/s3api/middlewares/acl-parser.go @@ -24,6 +24,7 @@ import ( "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/s3api/controllers" + "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3log" ) @@ -34,7 +35,6 @@ var ( func AclParser(be backend.Backend, logger s3log.AuditLogger, readonly bool) fiber.Handler { return func(ctx *fiber.Ctx) error { - isRoot, acct := ctx.Locals("isRoot").(bool), ctx.Locals("account").(auth.Account) path := ctx.Path() pathParts := strings.Split(path, "/") bucket := pathParts[1] @@ -53,6 +53,7 @@ func AclParser(be backend.Backend, logger s3log.AuditLogger, readonly bool) fibe !ctx.Request().URI().QueryArgs().Has("object-lock") && !ctx.Request().URI().QueryArgs().Has("ownershipControls") && !ctx.Request().URI().QueryArgs().Has("cors") { + isRoot, acct := utils.ContextKeyIsRoot.Get(ctx).(bool), utils.ContextKeyAccount.Get(ctx).(auth.Account) if err := auth.MayCreateBucket(acct, isRoot); err != nil { return controllers.SendXMLResponse(ctx, nil, err, &controllers.MetaOpts{Logger: logger, Action: "CreateBucket"}) } @@ -77,10 +78,10 @@ func AclParser(be backend.Backend, logger s3log.AuditLogger, readonly bool) fibe // if owner is not set, set default owner to root account if parsedAcl.Owner == "" { - parsedAcl.Owner = ctx.Locals("rootAccess").(string) + parsedAcl.Owner = utils.ContextKeyRootAccessKey.Get(ctx).(string) } - ctx.Locals("parsedAcl", parsedAcl) + utils.ContextKeyParsedAcl.Set(ctx, parsedAcl) return ctx.Next() } } diff --git a/s3api/middlewares/admin.go b/s3api/middlewares/admin.go index ad799a6..e5646cd 100644 --- a/s3api/middlewares/admin.go +++ b/s3api/middlewares/admin.go @@ -21,13 +21,14 @@ import ( "github.com/versity/versitygw/auth" "github.com/versity/versitygw/metrics" "github.com/versity/versitygw/s3api/controllers" + "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3log" ) func IsAdmin(logger s3log.AuditLogger) fiber.Handler { return func(ctx *fiber.Ctx) error { - acct := ctx.Locals("account").(auth.Account) + acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) if acct.Role != auth.RoleAdmin { path := ctx.Path() return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrAdminAccessDenied), diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index 6aacea7..54aa70a 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -46,14 +46,15 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au acct := accounts{root: root, iam: iam} return func(ctx *fiber.Ctx) error { - // If account is set in context locals, it means it was presigned url case - _, ok := ctx.Locals("account").(auth.Account) - if ok { + // The bucket is public, no need to check this signature + if utils.ContextKeyPublicBucket.IsSet(ctx) { + return ctx.Next() + } + // If ContextKeyAuthenticated is set in context locals, it means it was presigned url case + if utils.ContextKeyAuthenticated.IsSet(ctx) { return ctx.Next() } - ctx.Locals("region", region) - ctx.Locals("startTime", time.Now()) authorization := ctx.Get("Authorization") if authorization == "" { return sendResponse(ctx, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty), logger, mm) @@ -72,8 +73,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au }, logger, mm) } - ctx.Locals("isRoot", authData.Access == root.Access) - ctx.Locals("rootAccess", root.Access) + utils.ContextKeyIsRoot.Set(ctx, authData.Access == root.Access) account, err := acct.getAccount(authData.Access) if err == auth.ErrNoSuchUser { @@ -82,7 +82,8 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au if err != nil { return sendResponse(ctx, err, logger, mm) } - ctx.Locals("account", account) + + utils.ContextKeyAccount.Set(ctx, account) // Check X-Amz-Date header date := ctx.Get("X-Amz-Date") diff --git a/s3api/middlewares/body-reader.go b/s3api/middlewares/body-reader.go index aefd4fe..1c4ed50 100644 --- a/s3api/middlewares/body-reader.go +++ b/s3api/middlewares/body-reader.go @@ -18,14 +18,15 @@ import ( "io" "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/s3api/utils" ) func wrapBodyReader(ctx *fiber.Ctx, wr func(io.Reader) io.Reader) { - r, ok := ctx.Locals("body-reader").(io.Reader) + r, ok := utils.ContextKeyBodyReader.Get(ctx).(io.Reader) if !ok { r = ctx.Request().BodyStream() } r = wr(r) - ctx.Locals("body-reader", r) + utils.ContextKeyBodyReader.Set(ctx, r) } diff --git a/s3api/middlewares/presign-auth.go b/s3api/middlewares/presign-auth.go index eb86ccd..f9d285c 100644 --- a/s3api/middlewares/presign-auth.go +++ b/s3api/middlewares/presign-auth.go @@ -17,7 +17,6 @@ package middlewares import ( "io" "strconv" - "time" "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" @@ -31,20 +30,24 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, logger acct := accounts{root: root, iam: iam} return func(ctx *fiber.Ctx) error { + // The bucket is public, no need to check this signature + if utils.ContextKeyPublicBucket.IsSet(ctx) { + return ctx.Next() + } if ctx.Query("X-Amz-Signature") == "" { return ctx.Next() } - ctx.Locals("region", region) - ctx.Locals("startTime", time.Now()) + // Set in the context the "authenticated" key, in case the authentication succeeds, + // otherwise the middleware will return the caucht error + utils.ContextKeyAuthenticated.Set(ctx, true) authData, err := utils.ParsePresignedURIParts(ctx) if err != nil { return sendResponse(ctx, err, logger, mm) } - ctx.Locals("isRoot", authData.Access == root.Access) - ctx.Locals("rootAccess", root.Access) + utils.ContextKeyIsRoot.Set(ctx, authData.Access == root.Access) account, err := acct.getAccount(authData.Access) if err == auth.ErrNoSuchUser { @@ -53,7 +56,7 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, logger if err != nil { return sendResponse(ctx, err, logger, mm) } - ctx.Locals("account", account) + utils.ContextKeyAccount.Set(ctx, account) var contentLength int64 contentLengthStr := ctx.Get("Content-Length") diff --git a/s3api/middlewares/public-bucket.go b/s3api/middlewares/public-bucket.go new file mode 100644 index 0000000..567324f --- /dev/null +++ b/s3api/middlewares/public-bucket.go @@ -0,0 +1,298 @@ +// 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 middlewares + +import ( + "io" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/metrics" + "github.com/versity/versitygw/s3api/utils" + "github.com/versity/versitygw/s3err" + "github.com/versity/versitygw/s3log" +) + +func AuthorizePublicBucketAccess(be backend.Backend, l s3log.AuditLogger, mm *metrics.Manager) fiber.Handler { + return func(ctx *fiber.Ctx) error { + // skip for auhtneicated requests + if ctx.Query("X-Amz-Algorithm") != "" || ctx.Get("Authorization") != "" { + return ctx.Next() + } + + bucket, object := parsePath(ctx.Path()) + + action, permission, err := detectS3Action(ctx, object == "") + if err != nil { + return sendResponse(ctx, err, l, mm) + } + + err = auth.VerifyPublicAccess(ctx.Context(), be, action, permission, bucket, object) + if err != nil { + return sendResponse(ctx, err, l, mm) + } + + if utils.IsBigDataAction(ctx) { + payloadType := ctx.Get("X-Amz-Content-Sha256") + if utils.IsUnsignedStreamingPayload(payloadType) { + checksumType, err := utils.ExtractChecksumType(ctx) + if err != nil { + return sendResponse(ctx, err, l, mm) + } + + wrapBodyReader(ctx, func(r io.Reader) io.Reader { + var cr io.Reader + cr, err = utils.NewUnsignedChunkReader(r, checksumType) + return cr + }) + if err != nil { + return sendResponse(ctx, err, l, mm) + } + } + } + + utils.ContextKeyPublicBucket.Set(ctx, true) + + return ctx.Next() + } +} + +func detectS3Action(ctx *fiber.Ctx, isBucketAction bool) (auth.Action, auth.Permission, error) { + path := ctx.Path() + // ListBuckets is not publically available + if path == "/" { + //TODO: Still not clear what kind of error should be returned in this case(ListBuckets) + return "", auth.PermissionRead, s3err.GetAPIError(s3err.ErrAccessDenied) + } + + queryArgs := ctx.Context().QueryArgs() + + switch ctx.Method() { + case fiber.MethodPatch: + // Admin apis should always be protected + return "", "", s3err.GetAPIError(s3err.ErrAccessDenied) + case fiber.MethodHead: + // HeadBucket + if isBucketAction { + return auth.ListBucketAction, auth.PermissionRead, nil + } + + // HeadObject + return auth.GetObjectAction, auth.PermissionRead, nil + case fiber.MethodGet: + if isBucketAction { + if queryArgs.Has("tagging") { + // GetBucketTagging + return auth.GetBucketTaggingAction, auth.PermissionRead, nil + } else if queryArgs.Has("ownershipControls") { + // GetBucketOwnershipControls + return auth.GetBucketOwnershipControlsAction, auth.PermissionRead, s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership) + } else if queryArgs.Has("versioning") { + // GetBucketVersioning + return auth.GetBucketVersioningAction, auth.PermissionRead, nil + } else if queryArgs.Has("policy") { + // GetBucketPolicy + return auth.GetBucketPolicyAction, auth.PermissionRead, nil + } else if queryArgs.Has("cors") { + // GetBucketCors + return auth.GetBucketCorsAction, auth.PermissionRead, nil + } else if queryArgs.Has("versions") { + // ListObjectVersions + return auth.ListBucketVersionsAction, auth.PermissionRead, nil + } else if queryArgs.Has("object-lock") { + // GetObjectLockConfiguration + return auth.GetBucketObjectLockConfigurationAction, auth.PermissionReadAcp, nil + } else if queryArgs.Has("acl") { + // GetBucketAcl + return auth.GetBucketAclAction, auth.PermissionRead, nil + } else if queryArgs.Has("uploads") { + // ListMultipartUploads + return auth.ListBucketMultipartUploadsAction, auth.PermissionRead, nil + } else if queryArgs.GetUintOrZero("list-type") == 2 { + // ListObjectsV2 + return auth.ListBucketAction, auth.PermissionRead, nil + } + // All the other requests are considerd as ListObjects in the router + // no matter what kind of query arguments are provided apart from the ones above + + return auth.ListBucketAction, auth.PermissionRead, nil + } + + if queryArgs.Has("tagging") { + // GetObjectTagging + return auth.GetObjectTaggingAction, auth.PermissionRead, nil + } else if queryArgs.Has("retention") { + // GetObjectRetention + return auth.GetObjectRetentionAction, auth.PermissionRead, nil + } else if queryArgs.Has("legal-hold") { + // GetObjectLegalHold + return auth.GetObjectLegalHoldAction, auth.PermissionReadAcp, nil + } else if queryArgs.Has("acl") { + // GetObjectAcl + return auth.GetObjectAclAction, auth.PermissionRead, nil + } else if queryArgs.Has("attributes") { + // GetObjectAttributes + return auth.GetObjectAttributesAction, auth.PermissionRead, nil + } else if queryArgs.Has("uploadId") { + // ListParts + return auth.ListMultipartUploadPartsAction, auth.PermissionRead, nil + } + + // All the other requests are considerd as GetObject in the router + // no matter what kind of query arguments are provided apart from the ones above + if queryArgs.Has("versionId") { + return auth.GetObjectVersionAction, auth.PermissionRead, nil + } + return auth.GetObjectAction, auth.PermissionRead, nil + case fiber.MethodPut: + if isBucketAction { + if queryArgs.Has("tagging") { + // PutBucketTagging + return auth.PutBucketTaggingAction, auth.PermissionWrite, nil + } + if queryArgs.Has("ownershipControls") { + // PutBucketOwnershipControls + return auth.PutBucketOwnershipControlsAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership) + } + if queryArgs.Has("versioning") { + // PutBucketVersioning + return auth.PutBucketVersioningAction, auth.PermissionWrite, nil + } + if queryArgs.Has("object-lock") { + // PutObjectLockConfiguration + return auth.PutBucketObjectLockConfigurationAction, auth.PermissionWrite, nil + } + if queryArgs.Has("cors") { + // PutBucketCors + return auth.PutBucketCorsAction, auth.PermissionWrite, nil + } + if queryArgs.Has("policy") { + // PutBucketPolicy + return auth.PutBucketPolicyAction, auth.PermissionWrite, nil + } + if queryArgs.Has("acl") { + // PutBucketAcl + return auth.PutBucketAclAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousRequest) + } + + // All the other rquestes are considered as 'CreateBucket' in the router + return "", "", s3err.GetAPIError(s3err.ErrAnonymousRequest) + } + + if queryArgs.Has("tagging") { + // PutObjectTagging + return auth.PutObjectTaggingAction, auth.PermissionWrite, nil + } + if queryArgs.Has("retention") { + // PutObjectRetention + return auth.PutObjectRetentionAction, auth.PermissionWrite, nil + } + if queryArgs.Has("legal-hold") { + // PutObjectLegalHold + return auth.PutObjectLegalHoldAction, auth.PermissionWrite, nil + } + if queryArgs.Has("acl") { + // PutObjectAcl + return auth.PutObjectAclAction, auth.PermissionWriteAcp, s3err.GetAPIError(s3err.ErrAnonymousRequest) + } + if queryArgs.Has("uploadId") && queryArgs.Has("partNumber") { + if ctx.Get("X-Amz-Copy-Source") != "" { + // UploadPartCopy + //TODO: Add public access check for copy-source + // Return AccessDenied for now + return auth.PutObjectAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAccessDenied) + } + + utils.ContextKeyBodyReader.Set(ctx, ctx.Request().BodyStream()) + // UploadPart + return auth.PutObjectAction, auth.PermissionWrite, nil + } + if ctx.Get("X-Amz-Copy-Source") != "" { + return auth.PutObjectAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousCopyObject) + } + + utils.ContextKeyBodyReader.Set(ctx, ctx.Request().BodyStream()) + // All the other requests are considered as 'PutObject' in the router + return auth.PutObjectAction, auth.PermissionWrite, nil + case fiber.MethodPost: + if isBucketAction { + // DeleteObjects + // FIXME: should be fixed with https://github.com/versity/versitygw/issues/1327 + // Return AccessDenied for now + return auth.DeleteObjectAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAccessDenied) + } + + if queryArgs.Has("restore") { + return auth.RestoreObjectAction, auth.PermissionWrite, nil + } + if queryArgs.Has("select") && ctx.Query("select-type") == "2" { + // SelectObjectContent + return auth.GetObjectAction, auth.PermissionRead, s3err.GetAPIError(s3err.ErrAnonymousRequest) + } + if queryArgs.Has("uploadId") { + // CompleteMultipartUpload + return auth.PutObjectAction, auth.PermissionWrite, nil + } + + // All the other requests are considered as 'CreateMultipartUpload' in the router + return "", "", s3err.GetAPIError(s3err.ErrAnonymousCreateMp) + case fiber.MethodDelete: + if isBucketAction { + if queryArgs.Has("tagging") { + // DeleteBucketTagging + return auth.PutBucketTaggingAction, auth.PermissionWrite, nil + } + if queryArgs.Has("ownershipControls") { + // DeleteBucketOwnershipControls + return auth.PutBucketOwnershipControlsAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership) + } + if queryArgs.Has("policy") { + // DeleteBucketPolicy + return auth.PutBucketPolicyAction, auth.PermissionWrite, nil + } + if queryArgs.Has("cors") { + // DeleteBucketCors + return auth.PutBucketCorsAction, auth.PermissionWrite, nil + } + + // All the other requests are considered as 'DeleteBucket' in the router + return auth.DeleteBucketAction, auth.PermissionWrite, nil + } + + if queryArgs.Has("tagging") { + // DeleteObjectTagging + return auth.PutObjectTaggingAction, auth.PermissionWrite, nil + } + if queryArgs.Has("uploadId") { + // AbortMultipartUpload + return auth.AbortMultipartUploadAction, auth.PermissionWrite, nil + } + // All the other requests are considered as 'DeleteObject' in the router + return auth.DeleteObjectAction, auth.PermissionWrite, nil + default: + // In no action is detected, return AccessDenied ? + return "", "", s3err.GetAPIError(s3err.ErrAccessDenied) + } +} + +// parsePath extracts the bucket and object names from the path +func parsePath(path string) (string, string) { + p := strings.TrimPrefix(path, "/") + bucket, object, _ := strings.Cut(p, "/") + + return bucket, object +} diff --git a/s3api/middlewares/set-default-keys.go b/s3api/middlewares/set-default-keys.go new file mode 100644 index 0000000..e87cbbf --- /dev/null +++ b/s3api/middlewares/set-default-keys.go @@ -0,0 +1,37 @@ +// 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 middlewares + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/s3api/utils" +) + +func SetDefaultValues(root RootUserConfig, region string) fiber.Handler { + return func(ctx *fiber.Ctx) error { + // These are necessary for the server access logs + utils.ContextKeyRegion.Set(ctx, region) + utils.ContextKeyStartTime.Set(ctx, time.Now()) + utils.ContextKeyRootAccessKey.Set(ctx, root.Access) + // Set the account and isRoot to some defulat values, to avoid panics + // in case of public buckets + utils.ContextKeyAccount.Set(ctx, auth.Account{}) + utils.ContextKeyIsRoot.Set(ctx, false) + return ctx.Next() + } +} diff --git a/s3api/server.go b/s3api/server.go index 96bf7d1..c23b065 100644 --- a/s3api/server.go +++ b/s3api/server.go @@ -83,11 +83,17 @@ func New( app.Use(middlewares.HostStyleParser(server.virtualDomain)) } + // initilaze the default value setter middleware + app.Use(middlewares.SetDefaultValues(root, region)) + // initialize the debug logger in debug mode if server.debug { app.Use(middlewares.DebugLogger()) } + // Public buckets access checker + app.Use(middlewares.AuthorizePublicBucketAccess(be, l, mm)) + // Authentication middlewares app.Use(middlewares.VerifyPresignedV4Signature(root, iam, l, mm, region, server.debug)) app.Use(middlewares.VerifyV4Signature(root, iam, l, mm, region, server.debug)) diff --git a/s3api/utils/chunk-reader.go b/s3api/utils/chunk-reader.go index d960876..bba83fd 100644 --- a/s3api/utils/chunk-reader.go +++ b/s3api/utils/chunk-reader.go @@ -88,11 +88,28 @@ func (c checksumType) isValid() bool { c == checksumTypeCrc64nvme } +// Extracts and validates the checksum type from the 'X-Amz-Trailer' header +func ExtractChecksumType(ctx *fiber.Ctx) (checksumType, error) { + trailer := ctx.Get("X-Amz-Trailer") + chType := checksumType(strings.ToLower(trailer)) + if chType != "" && !chType.isValid() { + debuglogger.Logf("invalid value for 'X-Amz-Trailer': %v", chType) + return "", s3err.GetAPIError(s3err.ErrTrailerHeaderNotSupported) + } + + return chType, nil +} + // IsSpecialPayload checks for special authorization types func IsSpecialPayload(str string) bool { return specialValues[payloadType(str)] } +// Checks if the provided string is unsigned payload trailer type +func IsUnsignedStreamingPayload(str string) bool { + return payloadType(str) == payloadTypeStreamingUnsignedTrailer +} + // IsChunkEncoding checks for streaming/unsigned authorization types func IsStreamingPayload(str string) bool { pt := payloadType(str) @@ -126,9 +143,12 @@ func NewChunkReader(ctx *fiber.Ctx, r io.Reader, authdata AuthData, region, secr return nil, fmt.Errorf("invalid x-amz-content-sha256: %v", string(contentSha256)) } - checksumType := checksumType(strings.ToLower(ctx.Get("X-Amz-Trailer"))) - if contentSha256 != payloadTypeStreamingSigned && !checksumType.isValid() { - debuglogger.Logf("invalid value for 'X-Amz-Trailer': %v", checksumType) + checksumType, err := ExtractChecksumType(ctx) + if err != nil { + return nil, err + } + if contentSha256 != payloadTypeStreamingSigned && checksumType == "" { + debuglogger.Logf("empty value for required trailer header 'X-Amz-Trailer': %v", checksumType) return nil, s3err.GetAPIError(s3err.ErrTrailerHeaderNotSupported) } diff --git a/s3api/utils/context-keys.go b/s3api/utils/context-keys.go new file mode 100644 index 0000000..313248a --- /dev/null +++ b/s3api/utils/context-keys.go @@ -0,0 +1,65 @@ +// 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 utils + +import ( + "github.com/gofiber/fiber/v2" +) + +// Region, StartTime, IsRoot, Account, AccessKey context locals +// are set to defualut values in middlewares.SetDefaultValues +// to avoid the nil interface conversions +type ContextKey string + +const ( + ContextKeyRegion ContextKey = "region" + ContextKeyStartTime ContextKey = "start-time" + ContextKeyIsRoot ContextKey = "is-root" + ContextKeyRootAccessKey ContextKey = "root-access-key" + ContextKeyAccount ContextKey = "account" + ContextKeyAuthenticated ContextKey = "authenticated" + ContextKeyPublicBucket ContextKey = "public-bucket" + ContextKeyParsedAcl ContextKey = "parsed-acl" + ContextKeySkipResBodyLog ContextKey = "skip-res-body-log" + ContextKeyBodyReader ContextKey = "body-reader" +) + +func (ck ContextKey) Values() []ContextKey { + return []ContextKey{ + ContextKeyRegion, + ContextKeyStartTime, + ContextKeyIsRoot, + ContextKeyRootAccessKey, + ContextKeyAccount, + ContextKeyAuthenticated, + ContextKeyPublicBucket, + ContextKeyParsedAcl, + ContextKeySkipResBodyLog, + ContextKeyBodyReader, + } +} + +func (ck ContextKey) Set(ctx *fiber.Ctx, val any) { + ctx.Locals(string(ck), val) +} + +func (ck ContextKey) IsSet(ctx *fiber.Ctx) bool { + val := ctx.Locals(string(ck)) + return val != nil +} + +func (ck ContextKey) Get(ctx *fiber.Ctx) any { + return ctx.Locals(string(ck)) +} diff --git a/s3api/utils/presign-auth-reader.go b/s3api/utils/presign-auth-reader.go index fd2938b..e2531e9 100644 --- a/s3api/utils/presign-auth-reader.go +++ b/s3api/utils/presign-auth-reader.go @@ -180,7 +180,7 @@ func ParsePresignedURIParts(ctx *fiber.Ctx) (AuthData, error) { return a, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch) } - if ctx.Locals("region") != creds[2] { + if ContextKeyRegion.Get(ctx) != creds[2] { return a, s3err.APIError{ Code: "SignatureDoesNotMatch", Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", creds[2]), diff --git a/s3err/s3err.go b/s3err/s3err.go index 64253e3..0d8b41f 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -59,6 +59,11 @@ type ErrorCode int const ( ErrNone ErrorCode = iota ErrAccessDenied + ErrAnonymousRequest + ErrAnonymousCreateMp + ErrAnonymousCopyObject + ErrAnonymousPutBucketOwnership + ErrAnonymousGetBucketOwnership ErrMethodNotAllowed ErrBucketNotEmpty ErrVersionedBucketNotEmpty @@ -186,6 +191,31 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Access Denied.", HTTPStatusCode: http.StatusForbidden, }, + ErrAnonymousRequest: { + Code: "AccessDenied", + Description: "Anonymous users cannot invoke this API. Please authenticate.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAnonymousCreateMp: { + Code: "AccessDenied", + Description: "Anonymous users cannot initiate multipart uploads. Please authenticate.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAnonymousCopyObject: { + Code: "AccessDenied", + Description: "Anonymous users cannot copy objects. Please authenticate.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAnonymousPutBucketOwnership: { + Code: "AccessDenied", + Description: "s3:PutBucketOwnershipControls does not support Anonymous requests!", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAnonymousGetBucketOwnership: { + Code: "AccessDenied", + Description: "s3:GetBucketOwnershipControls does not support Anonymous requests!", + HTTPStatusCode: http.StatusForbidden, + }, ErrMethodNotAllowed: { Code: "MethodNotAllowed", Description: "The specified method is not allowed against this resource.", diff --git a/s3event/event.go b/s3event/event.go index 32c49f0..37bac42 100644 --- a/s3event/event.go +++ b/s3event/event.go @@ -22,6 +22,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/s3api/utils" ) type S3EventSender interface { @@ -147,14 +148,14 @@ func createEventSchema(ctx *fiber.Ctx, meta EventMeta, configId ConfigurationId) bucket, object = path[1], strings.Join(path[2:], "/") } - acc := ctx.Locals("account").(auth.Account) + acc := utils.ContextKeyAccount.Get(ctx).(auth.Account) return EventSchema{ Records: []EventRecord{ { EventVersion: "2.2", EventSource: "aws:s3", - AwsRegion: ctx.Locals("region").(string), + AwsRegion: utils.ContextKeyRegion.Get(ctx).(string), EventTime: time.Now().Format(time.RFC3339), EventName: meta.EventName, UserIdentity: EventUserIdentity{ diff --git a/s3log/file.go b/s3log/file.go index 277645b..42f3005 100644 --- a/s3log/file.go +++ b/s3log/file.go @@ -24,6 +24,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3err" ) @@ -74,7 +75,7 @@ func (f *FileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) { } errorCode := "" httpStatus := 200 - startTime, ok := ctx.Locals("startTime").(time.Time) + startTime, ok := utils.ContextKeyStartTime.Get(ctx).(time.Time) if !ok { startTime = time.Now() } @@ -95,9 +96,9 @@ func (f *FileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) { } } - switch ctx.Locals("account").(type) { - case auth.Account: - access = ctx.Locals("account").(auth.Account).Access + acct, ok := utils.ContextKeyAccount.Get(ctx).(auth.Account) + if ok { + access = acct.Access } lf.BucketOwner = meta.BucketOwner @@ -121,7 +122,7 @@ func (f *FileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) { lf.HostID = ctx.Get("X-Amz-Id-2") lf.SignatureVersion = "SigV4" lf.AuthenticationType = "AuthHeader" - lf.HostHeader = fmt.Sprintf("s3.%v.amazonaws.com", ctx.Locals("region").(string)) + lf.HostHeader = fmt.Sprintf("s3.%v.amazonaws.com", utils.ContextKeyRegion.Get(ctx).(string)) lf.AccessPointARN = fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/")) lf.AclRequired = "Yes" diff --git a/s3log/file_admin.go b/s3log/file_admin.go index 73aa256..9b6f812 100644 --- a/s3log/file_admin.go +++ b/s3log/file_admin.go @@ -22,6 +22,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/s3api/utils" ) // FileLogger is a local file audit log @@ -57,7 +58,10 @@ func (f *AdminFileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMe access := "-" reqURI := ctx.OriginalURL() errorCode := "" - startTime := ctx.Locals("startTime").(time.Time) + startTime, ok := utils.ContextKeyStartTime.Get(ctx).(time.Time) + if !ok { + startTime = time.Now() + } tlsConnState := ctx.Context().TLSConnectionState() if tlsConnState != nil { lf.CipherSuite = tls.CipherSuiteName(tlsConnState.CipherSuite) @@ -68,9 +72,9 @@ func (f *AdminFileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMe errorCode = err.Error() } - switch ctx.Locals("account").(type) { + switch utils.ContextKeyAccount.Get(ctx).(type) { case auth.Account: - access = ctx.Locals("account").(auth.Account).Access + access = utils.ContextKeyAccount.Get(ctx).(auth.Account).Access } lf.Time = time.Now() diff --git a/s3log/webhook.go b/s3log/webhook.go index c0e52af..989977e 100644 --- a/s3log/webhook.go +++ b/s3log/webhook.go @@ -28,6 +28,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3err" ) @@ -71,7 +72,10 @@ func (wl *WebhookLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMet } errorCode := "" httpStatus := 200 - startTime := ctx.Locals("startTime").(time.Time) + startTime, ok := utils.ContextKeyStartTime.Get(ctx).(time.Time) + if !ok { + startTime = time.Now() + } tlsConnState := ctx.Context().TLSConnectionState() if tlsConnState != nil { lf.CipherSuite = tls.CipherSuiteName(tlsConnState.CipherSuite) @@ -89,9 +93,9 @@ func (wl *WebhookLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMet } } - switch ctx.Locals("account").(type) { - case auth.Account: - access = ctx.Locals("account").(auth.Account).Access + acct, ok := utils.ContextKeyAccount.Get(ctx).(auth.Account) + if ok { + access = acct.Access } lf.BucketOwner = meta.BucketOwner @@ -115,7 +119,7 @@ func (wl *WebhookLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMet lf.HostID = ctx.Get("X-Amz-Id-2") lf.SignatureVersion = "SigV4" lf.AuthenticationType = "AuthHeader" - lf.HostHeader = fmt.Sprintf("s3.%v.amazonaws.com", ctx.Locals("region").(string)) + lf.HostHeader = fmt.Sprintf("s3.%v.amazonaws.com", utils.ContextKeyRegion.Get(ctx).(string)) lf.AccessPointARN = fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/")) lf.AclRequired = "Yes" diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 6c8195f..deec793 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -15,7 +15,6 @@ package integration func TestAuthentication(s *S3Conf) { - Authentication_empty_auth_header(s) Authentication_invalid_auth_header(s) Authentication_unsupported_signature_version(s) Authentication_malformed_credentials(s) @@ -37,7 +36,6 @@ func TestAuthentication(s *S3Conf) { } func TestPresignedAuthentication(s *S3Conf) { - PresignedAuth_missing_algo_query_param(s) PresignedAuth_unsupported_algorithm(s) PresignedAuth_missing_credentials_query_param(s) PresignedAuth_malformed_creds_invalid_parts(s) @@ -636,6 +634,11 @@ func TestFullFlow(s *S3Conf) { TestGetObjectLegalHold(s) TestWORMProtection(s) TestAccessControl(s) + // FIXME: The tests should pass for azure as well + // but this issue should be fixed with https://github.com/versity/versitygw/issues/1336 + if !s.azureTests { + TestPublicBuckets(s) + } if s.versioningEnabled { TestVersioning(s) } @@ -775,6 +778,13 @@ func TestAccessControl(s *S3Conf) { AccessControl_copy_object_with_starting_slash_for_user(s) } +func TestPublicBuckets(s *S3Conf) { + PublicBucket_default_privet_bucket(s) + PublicBucket_public_bucket_policy(s) + PublicBucket_public_object_policy(s) + PublicBucket_public_acl(s) +} + func TestVersioning(s *S3Conf) { // PutBucketVersioning action PutBucketVersioning_non_existing_bucket(s) @@ -863,7 +873,6 @@ type IntTests map[string]func(s *S3Conf) error func GetIntTests() IntTests { return IntTests{ - "Authentication_empty_auth_header": Authentication_empty_auth_header, "Authentication_invalid_auth_header": Authentication_invalid_auth_header, "Authentication_unsupported_signature_version": Authentication_unsupported_signature_version, "Authentication_malformed_credentials": Authentication_malformed_credentials, @@ -882,7 +891,6 @@ func GetIntTests() IntTests { "Authentication_incorrect_payload_hash": Authentication_incorrect_payload_hash, "Authentication_incorrect_md5": Authentication_incorrect_md5, "Authentication_signature_error_incorrect_secret_key": Authentication_signature_error_incorrect_secret_key, - "PresignedAuth_missing_algo_query_param": PresignedAuth_missing_algo_query_param, "PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm, "PresignedAuth_missing_credentials_query_param": PresignedAuth_missing_credentials_query_param, "PresignedAuth_malformed_creds_invalid_parts": PresignedAuth_malformed_creds_invalid_parts, @@ -1277,6 +1285,10 @@ func GetIntTests() IntTests { "AccessControl_root_PutBucketAcl": AccessControl_root_PutBucketAcl, "AccessControl_user_PutBucketAcl_with_policy_access": AccessControl_user_PutBucketAcl_with_policy_access, "AccessControl_copy_object_with_starting_slash_for_user": AccessControl_copy_object_with_starting_slash_for_user, + "PublicBucket_default_privet_bucket": PublicBucket_default_privet_bucket, + "PublicBucket_public_bucket_policy": PublicBucket_public_bucket_policy, + "PublicBucket_public_object_policy": PublicBucket_public_object_policy, + "PublicBucket_public_acl": PublicBucket_public_acl, "PutBucketVersioning_non_existing_bucket": PutBucketVersioning_non_existing_bucket, "PutBucketVersioning_invalid_status": PutBucketVersioning_invalid_status, "PutBucketVersioning_success_enabled": PutBucketVersioning_success_enabled, diff --git a/tests/integration/s3conf.go b/tests/integration/s3conf.go index 10fe585..19b8d26 100644 --- a/tests/integration/s3conf.go +++ b/tests/integration/s3conf.go @@ -130,16 +130,24 @@ func (c *S3Conf) GetClient() *s3.Client { }) } +func (c *S3Conf) GetAnonymousClient() *s3.Client { + cfg := c.Config() + cfg.Credentials = aws.AnonymousCredentials{} + return s3.NewFromConfig(cfg, func(o *s3.Options) { + if c.hostStyle { + o.BaseEndpoint = &c.endpoint + o.UsePathStyle = false + } + }) +} + func (c *S3Conf) Config() aws.Config { creds := c.getCreds() - tr := &http.Transport{} - client := &http.Client{Transport: tr} - opts := []func(*config.LoadOptions) error{ config.WithRegion(c.awsRegion), config.WithCredentialsProvider(creds), - config.WithHTTPClient(client), + config.WithHTTPClient(c.httpClient), config.WithRetryMaxAttempts(1), } diff --git a/tests/integration/tests.go b/tests/integration/tests.go index e71dc0e..a235121 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -47,31 +47,6 @@ var ( nullVersionId = "null" ) -func Authentication_empty_auth_header(s *S3Conf) error { - testName := "Authentication_empty_auth_header" - return authHandler(s, &authConfig{ - testName: testName, - path: "my-bucket", - method: http.MethodGet, - body: nil, - service: "s3", - date: time.Now(), - }, func(req *http.Request) error { - req.Header.Set("Authorization", "") - - resp, err := s.httpClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty)); err != nil { - return err - } - - return nil - }) -} - func Authentication_invalid_auth_header(s *S3Conf) error { testName := "Authentication_invalid_auth_header" return authHandler(s, &authConfig{ @@ -579,43 +554,6 @@ func Authentication_signature_error_incorrect_secret_key(s *S3Conf) error { }) } -func PresignedAuth_missing_algo_query_param(s *S3Conf) error { - testName := "PresignedAuth_missing_algo_query_param" - return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { - ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) - v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) - cancel() - if err != nil { - return err - } - - urlParsed, err := url.Parse(v4req.URL) - if err != nil { - return err - } - - queries := urlParsed.Query() - queries.Del("X-Amz-Algorithm") - urlParsed.RawQuery = queries.Encode() - - req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) - if err != nil { - return err - } - - resp, err := s.httpClient.Do(req) - if err != nil { - return err - } - - if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil { - return err - } - - return nil - }) -} - func PresignedAuth_unsupported_algorithm(s *S3Conf) error { testName := "PresignedAuth_unsupported_algorithm" return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { @@ -15335,6 +15273,2420 @@ func AccessControl_copy_object_with_starting_slash_for_user(s *S3Conf) error { }) } +// Public bucket tests +func PublicBucket_default_privet_bucket(s *S3Conf) error { + testName := "PublicBucket_default_privet_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + partNumber := int32(1) + + for _, test := range []PublicBucketTestCase{ + { + Action: "ListBuckets", + Call: func(ctx context.Context) error { + _, err := s3client.ListBuckets(ctx, &s3.ListBucketsInput{}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "HeadBucket", + Call: func(ctx context.Context) error { + _, err := s3client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CreateBucket", + Call: func(ctx context.Context) error { + _, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: getPtr("new-bucket")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{ + Bucket: &bucket, + ACL: types.BucketCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "DeleteBucket", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutBucketVersioning", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ + Bucket: &bucket, + VersioningConfiguration: &types.VersioningConfiguration{ + Status: types.BucketVersioningStatusSuspended, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketVersioning", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{Bucket: &bucket, Policy: getPtr("{}")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketPolicy(ctx, &s3.DeleteBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{ + Bucket: &bucket, + OwnershipControls: &types.OwnershipControls{ + Rules: []types.OwnershipControlsRule{ + { + ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "GetBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership), + }, + { + Action: "DeleteBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketOwnershipControls(ctx, &s3.DeleteBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "PutBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketCors(ctx, &s3.PutBucketCorsInput{ + Bucket: &bucket, + CORSConfiguration: &types.CORSConfiguration{ + CORSRules: []types.CORSRule{ + { + AllowedMethods: []string{http.MethodPut}, + AllowedOrigins: []string{"my origin"}, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketCors(ctx, &s3.GetBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CreateMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCreateMp), + }, + { + Action: "CompleteMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key"), UploadId: getPtr("upload-id")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "AbortMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key"), UploadId: getPtr("upload-id")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "ListMultipartUploads", + Call: func(ctx context.Context) error { + _, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "ListParts", + Call: func(ctx context.Context) error { + _, err := s3client.ListParts(ctx, &s3.ListPartsInput{Bucket: &bucket, Key: getPtr("object-key"), UploadId: getPtr("upload-id")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "UploadPart", + Call: func(ctx context.Context) error { + _, err := s3client.UploadPart(ctx, &s3.UploadPartInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + UploadId: getPtr("upload-id"), + PartNumber: &partNumber, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "UploadPartCopy", + Call: func(ctx context.Context) error { + _, err := s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + UploadId: getPtr("upload-id"), + PartNumber: &partNumber, + CopySource: getPtr("source-bucket/source-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObject", + Call: func(ctx context.Context) error { + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "HeadObject", + Call: func(ctx context.Context) error { + _, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAcl(ctx, &s3.GetObjectAclInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectAttributes", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesEtag, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CopyObject", + Call: func(ctx context.Context) error { + _, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: &bucket, + Key: getPtr("copy-key"), + CopySource: getPtr("bucket-name/object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCopyObject), + }, + { + Action: "ListObjects", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjects(ctx, &s3.ListObjectsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "ListObjectsV2", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteObject", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteObjects", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: &bucket, + Delete: &types.Delete{ + Objects: []types.ObjectIdentifier{ + {Key: getPtr("object-key")}, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectAcl(ctx, &s3.PutObjectAclInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ACL: types.ObjectCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "ListObjectVersions", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "RestoreObject", + Call: func(ctx context.Context) error { + _, err := s3client.RestoreObject(ctx, &s3.RestoreObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + RestoreRequest: &types.RestoreRequest{ + Days: aws.Int32(1), + GlacierJobParameters: &types.GlacierJobParameters{ + Tier: types.TierStandard, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "SelectObjectContent", + Call: func(ctx context.Context) error { + _, err := s3client.SelectObjectContent(ctx, &s3.SelectObjectContentInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ExpressionType: types.ExpressionTypeSql, + Expression: getPtr("SELECT * FROM S3Object"), + InputSerialization: &types.InputSerialization{ + CSV: &types.CSVInput{}, + }, + OutputSerialization: &types.OutputSerialization{ + CSV: &types.CSVOutput{}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "GetBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketTagging(ctx, &s3.PutBucketTaggingInput{ + Bucket: &bucket, + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketTagging(ctx, &s3.DeleteBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Days: aws.Int32(1), + Mode: types.ObjectLockRetentionModeCompliance, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: aws.Time(time.Now().Add(24 * time.Hour)), + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + err := test.Call(ctx) + cancel() + if err == nil && test.ExpectedErr != nil { + return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr) + } + if err != nil { + if test.ExpectedErr == nil { + return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err) + } + + apiErr, ok := test.ExpectedErr.(s3err.APIError) + if !ok { + return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError") + } + + // The head requests doesn't have request body, thus only the status needs to be checked + if test.Action == "HeadBucket" || test.Action == "HeadObject" { + if err := checkSdkApiErr(err, http.StatusText(apiErr.HTTPStatusCode)); err != nil { + return err + } + continue + } + + if err := checkApiErr(err, apiErr); err != nil { + return err + } + } + } + + return nil + }, withAnonymousClient()) +} + +func PublicBucket_public_bucket_policy(s *S3Conf) error { + testName := "PublicBucket_public_bucket_policy" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + rootClient := s.GetClient() + // Grant public access to the bucket for bucket operations + err := grantPublicBucketPolicy(rootClient, bucket, policyTypeBucket) + if err != nil { + return err + } + partNumber := int32(1) + + for _, test := range []PublicBucketTestCase{ + { + Action: "ListBuckets", + Call: func(ctx context.Context) error { + _, err := s3client.ListBuckets(ctx, &s3.ListBucketsInput{}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "HeadBucket", + Call: func(ctx context.Context) error { + _, err := s3client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &bucket}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "CreateBucket", + Call: func(ctx context.Context) error { + _, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: getPtr("new-bucket")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{ + Bucket: &bucket, + ACL: types.BucketCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{Bucket: &bucket, Policy: getPtr("{}")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrMethodNotAllowed), + }, + { + Action: "GetBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrMethodNotAllowed), + }, + { + Action: "DeleteBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketPolicy(ctx, &s3.DeleteBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrMethodNotAllowed), + }, + { + Action: "PutBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{ + Bucket: &bucket, + OwnershipControls: &types.OwnershipControls{ + Rules: []types.OwnershipControlsRule{ + { + ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "GetBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership), + }, + { + Action: "DeleteBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketOwnershipControls(ctx, &s3.DeleteBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "PutBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketCors(ctx, &s3.PutBucketCorsInput{ + Bucket: &bucket, + CORSConfiguration: &types.CORSConfiguration{ + CORSRules: []types.CORSRule{ + { + AllowedMethods: []string{http.MethodPut}, + AllowedOrigins: []string{"my origin"}, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrNotImplemented), + }, + { + Action: "GetBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketCors(ctx, &s3.GetBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrNotImplemented), + }, + { + Action: "DeleteBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrNotImplemented), + }, + { + Action: "CreateMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCreateMp), + }, + { + Action: "CompleteMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key"), UploadId: getPtr("upload-id")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "AbortMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key"), UploadId: getPtr("upload-id")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "ListMultipartUploads", + Call: func(ctx context.Context) error { + _, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "ListParts", + Call: func(ctx context.Context) error { + _, err := s3client.ListParts(ctx, &s3.ListPartsInput{Bucket: &bucket, Key: getPtr("object-key"), UploadId: getPtr("upload-id")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "UploadPart", + Call: func(ctx context.Context) error { + _, err := s3client.UploadPart(ctx, &s3.UploadPartInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + UploadId: getPtr("upload-id"), + PartNumber: &partNumber, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "UploadPartCopy", + Call: func(ctx context.Context) error { + _, err := s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + UploadId: getPtr("upload-id"), + PartNumber: &partNumber, + CopySource: getPtr("source-bucket/source-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObject", + Call: func(ctx context.Context) error { + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "HeadObject", + Call: func(ctx context.Context) error { + _, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAcl(ctx, &s3.GetObjectAclInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectAttributes", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesEtag, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CopyObject", + Call: func(ctx context.Context) error { + _, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: &bucket, + Key: getPtr("copy-key"), + CopySource: getPtr("bucket-name/object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCopyObject), + }, + { + Action: "ListObjects", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjects(ctx, &s3.ListObjectsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "ListObjectsV2", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "DeleteObject", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteObjects", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: &bucket, + Delete: &types.Delete{ + Objects: []types.ObjectIdentifier{ + {Key: getPtr("object-key")}, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectAcl(ctx, &s3.PutObjectAclInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ACL: types.ObjectCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "ListObjectVersions", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "RestoreObject", + Call: func(ctx context.Context) error { + _, err := s3client.RestoreObject(ctx, &s3.RestoreObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + RestoreRequest: &types.RestoreRequest{ + Days: aws.Int32(1), + GlacierJobParameters: &types.GlacierJobParameters{ + Tier: types.TierStandard, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "SelectObjectContent", + Call: func(ctx context.Context) error { + _, err := s3client.SelectObjectContent(ctx, &s3.SelectObjectContentInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ExpressionType: types.ExpressionTypeSql, + Expression: getPtr("SELECT * FROM S3Object"), + InputSerialization: &types.InputSerialization{ + CSV: &types.CSVInput{}, + }, + OutputSerialization: &types.OutputSerialization{ + CSV: &types.CSVOutput{}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketTagging(ctx, &s3.PutBucketTaggingInput{ + Bucket: &bucket, + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "DeleteBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketTagging(ctx, &s3.DeleteBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Days: aws.Int32(1), + Mode: types.ObjectLockRetentionModeGovernance, + }, + }, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "PutObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: aws.Time(time.Now().Add(24 * time.Hour)), + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucket", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: nil, + }, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + err := test.Call(ctx) + cancel() + if err == nil && test.ExpectedErr != nil { + return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr) + } + if err != nil { + if test.ExpectedErr == nil { + return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err) + } + + apiErr, ok := test.ExpectedErr.(s3err.APIError) + if !ok { + return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError") + } + + // The head requests doesn't have request body, thus only the status needs to be checked + if test.Action == "HeadBucket" || test.Action == "HeadObject" { + if err := checkSdkApiErr(err, http.StatusText(apiErr.HTTPStatusCode)); err != nil { + return err + } + continue + } + + if err := checkApiErr(err, apiErr); err != nil { + return err + } + } + } + + return nil + }, withAnonymousClient(), withLock(), withSkipTearDown()) +} + +func PublicBucket_public_object_policy(s *S3Conf) error { + testName := "PublicBucket_public_object_policy" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + rootClient := s.GetClient() + // Grant public access to the bucket for bucket operations + err := grantPublicBucketPolicy(rootClient, bucket, policyTypeObject) + if err != nil { + return err + } + + mpKey := "my-mp" + + mp1, err := createMp(rootClient, bucket, mpKey) + if err != nil { + return err + } + + mp2, err := createMp(rootClient, bucket, mpKey) + if err != nil { + return err + } + + partNumber := int32(1) + var partEtag *string + + for _, test := range []PublicBucketTestCase{ + { + Action: "ListBuckets", + Call: func(ctx context.Context) error { + _, err := s3client.ListBuckets(ctx, &s3.ListBucketsInput{}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "HeadBucket", + Call: func(ctx context.Context) error { + _, err := s3client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CreateBucket", + Call: func(ctx context.Context) error { + _, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: getPtr("new-bucket")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{ + Bucket: &bucket, + ACL: types.BucketCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{Bucket: &bucket, Policy: getPtr("{}")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketPolicy(ctx, &s3.DeleteBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{ + Bucket: &bucket, + OwnershipControls: &types.OwnershipControls{ + Rules: []types.OwnershipControlsRule{ + { + ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "GetBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership), + }, + { + Action: "DeleteBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketOwnershipControls(ctx, &s3.DeleteBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "PutBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketCors(ctx, &s3.PutBucketCorsInput{ + Bucket: &bucket, + CORSConfiguration: &types.CORSConfiguration{ + CORSRules: []types.CORSRule{ + { + AllowedMethods: []string{http.MethodPut}, + AllowedOrigins: []string{"my origin"}, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketCors(ctx, &s3.GetBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CreateMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCreateMp), + }, + { + Action: "AbortMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{ + Bucket: &bucket, + Key: &mpKey, + UploadId: mp1.UploadId, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "ListMultipartUploads", + Call: func(ctx context.Context) error { + _, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "ListParts", + Call: func(ctx context.Context) error { + _, err := s3client.ListParts(ctx, &s3.ListPartsInput{ + Bucket: &bucket, + Key: &mpKey, + UploadId: mp2.UploadId, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "UploadPart", + Call: func(ctx context.Context) error { + partBuffer := make([]byte, 5*1024*1024) + rand.Read(partBuffer) + res, err := s3client.UploadPart(ctx, &s3.UploadPartInput{ + Bucket: &bucket, + Key: &mpKey, + UploadId: mp2.UploadId, + PartNumber: &partNumber, + Body: bytes.NewReader(partBuffer), + }) + if err == nil { + partEtag = res.ETag + } + return err + }, + ExpectedErr: nil, + }, + //FIXME: should be fixed after implementing the source bucket public access check + // return AccessDenied for now + { + Action: "UploadPartCopy", + Call: func(ctx context.Context) error { + _, err := s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{ + Bucket: &bucket, + Key: &mpKey, + UploadId: mp2.UploadId, + PartNumber: &partNumber, + CopySource: getPtr("source-bucket/source-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CompleteMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ + Bucket: &bucket, + Key: &mpKey, + UploadId: mp2.UploadId, + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: []types.CompletedPart{ + { + ETag: partEtag, + PartNumber: &partNumber, + }, + }, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "PutObject", + Call: func(ctx context.Context) error { + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &mpKey, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "HeadObject", + Call: func(ctx context.Context) error { + _, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &mpKey}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: &mpKey}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAcl(ctx, &s3.GetObjectAclInput{ + Bucket: &bucket, + Key: &mpKey, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrNotImplemented), + }, + { + Action: "GetObjectAttributes", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: &mpKey, + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesEtag, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "CopyObject", + Call: func(ctx context.Context) error { + _, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: &bucket, + Key: getPtr("copy-key"), + CopySource: getPtr("bucket-name/object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCopyObject), + }, + { + Action: "ListObjects", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjects(ctx, &s3.ListObjectsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "ListObjectsV2", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + // FIXME: should be fixed with https://github.com/versity/versitygw/issues/1327 + { + Action: "DeleteObjects", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: &bucket, + Delete: &types.Delete{ + Objects: []types.ObjectIdentifier{ + {Key: getPtr("object-key")}, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectAcl(ctx, &s3.PutObjectAclInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ACL: types.ObjectCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "ListObjectVersions", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "RestoreObject", + Call: func(ctx context.Context) error { + _, err := s3client.RestoreObject(ctx, &s3.RestoreObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + RestoreRequest: &types.RestoreRequest{ + Days: aws.Int32(1), + GlacierJobParameters: &types.GlacierJobParameters{ + Tier: types.TierStandard, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrNotImplemented), + }, + { + Action: "SelectObjectContent", + Call: func(ctx context.Context) error { + _, err := s3client.SelectObjectContent(ctx, &s3.SelectObjectContentInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ExpressionType: types.ExpressionTypeSql, + Expression: getPtr("SELECT * FROM S3Object"), + InputSerialization: &types.InputSerialization{ + CSV: &types.CSVInput{}, + }, + OutputSerialization: &types.OutputSerialization{ + CSV: &types.CSVOutput{}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketTagging(ctx, &s3.PutBucketTaggingInput{ + Bucket: &bucket, + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketTagging(ctx, &s3.DeleteBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{ + Bucket: &bucket, + Key: &mpKey, + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: &mpKey, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "DeleteObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{ + Bucket: &bucket, + Key: &mpKey, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "PutObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Days: aws.Int32(1), + Mode: types.ObjectLockRetentionModeGovernance, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &mpKey, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + RetainUntilDate: aws.Time(time.Now().Add(24 * time.Hour)), + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: &mpKey, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "PutObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &mpKey, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOff, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: &mpKey, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "DeleteObject", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &mpKey, + BypassGovernanceRetention: getBoolPtr(true), + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "DeleteBucket", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + err := test.Call(ctx) + cancel() + if err == nil && test.ExpectedErr != nil { + return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr) + } + if err != nil { + if test.ExpectedErr == nil { + return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err) + } + + apiErr, ok := test.ExpectedErr.(s3err.APIError) + if !ok { + return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError") + } + + // The head requests doesn't have request body, thus only the status needs to be checked + if test.Action == "HeadBucket" || test.Action == "HeadObject" { + if err := checkSdkApiErr(err, http.StatusText(apiErr.HTTPStatusCode)); err != nil { + return err + } + continue + } + + if err := checkApiErr(err, apiErr); err != nil { + return err + } + } + } + + // disable object lock on the bucket + err = changeBucketObjectLockStatus(rootClient, bucket, false) + if err != nil { + return err + } + + return nil + }, withAnonymousClient(), withLock()) +} + +func PublicBucket_public_acl(s *S3Conf) error { + testName := "PublicBucket_public_acl" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + partNumber := int32(1) + var etag *string + obj := "my-obj" + + // grant public access with acl + rootClient := s.GetClient() + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := rootClient.PutBucketAcl(ctx, &s3.PutBucketAclInput{ + Bucket: &bucket, + ACL: types.BucketCannedACLPublicReadWrite, + }) + cancel() + if err != nil { + return err + } + + mp, err := createMp(rootClient, bucket, obj) + if err != nil { + return err + } + + for _, test := range []PublicBucketTestCase{ + { + Action: "ListBuckets", + Call: func(ctx context.Context) error { + _, err := s3client.ListBuckets(ctx, &s3.ListBucketsInput{}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "HeadBucket", + Call: func(ctx context.Context) error { + _, err := s3client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CreateBucket", + Call: func(ctx context.Context) error { + _, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: getPtr("new-bucket")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "PutBucketAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{ + Bucket: &bucket, + ACL: types.BucketCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "DeleteBucket", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + //FIXME: implement tests for versioning enabled gateway + // { + // Action: "PutBucketVersioning", + // Call: func(ctx context.Context) error { + // _, err := s3client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ + // Bucket: &bucket, + // VersioningConfiguration: &types.VersioningConfiguration{ + // Status: types.BucketVersioningStatusSuspended, + // }, + // }) + // return err + // }, + // ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + // }, + // { + // Action: "GetBucketVersioning", + // Call: func(ctx context.Context) error { + // _, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{Bucket: &bucket}) + // return err + // }, + // ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + // }, + { + Action: "PutBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{Bucket: &bucket, Policy: getPtr("{}")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketPolicy", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketPolicy(ctx, &s3.DeleteBucketPolicyInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketOwnershipControls(ctx, &s3.PutBucketOwnershipControlsInput{ + Bucket: &bucket, + OwnershipControls: &types.OwnershipControls{ + Rules: []types.OwnershipControlsRule{ + { + ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "GetBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketOwnershipControls(ctx, &s3.GetBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership), + }, + { + Action: "DeleteBucketOwnershipControls", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketOwnershipControls(ctx, &s3.DeleteBucketOwnershipControlsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership), + }, + { + Action: "PutBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketCors(ctx, &s3.PutBucketCorsInput{ + Bucket: &bucket, + CORSConfiguration: &types.CORSConfiguration{ + CORSRules: []types.CORSRule{ + { + AllowedMethods: []string{http.MethodPut}, + AllowedOrigins: []string{"my origin"}, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketCors(ctx, &s3.GetBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketCors", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CreateMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{Bucket: &bucket, Key: getPtr("object-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCreateMp), + }, + { + Action: "AbortMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + UploadId: mp.UploadId, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "ListMultipartUploads", + Call: func(ctx context.Context) error { + _, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{Bucket: &bucket}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "ListParts", + Call: func(ctx context.Context) error { + _, err := s3client.ListParts(ctx, &s3.ListPartsInput{Bucket: &bucket, Key: getPtr("object-key"), UploadId: getPtr("upload-id")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "UploadPart", + Call: func(ctx context.Context) error { + partBuffer := make([]byte, 5*1024*1024) + rand.Read(partBuffer) + res, err := s3client.UploadPart(ctx, &s3.UploadPartInput{ + Bucket: &bucket, + Key: &obj, + UploadId: mp.UploadId, + PartNumber: &partNumber, + Body: bytes.NewReader(partBuffer), + }) + if err == nil { + etag = res.ETag + } + return err + }, + ExpectedErr: nil, + }, + //FIXME: should be fixed after implementing the source bucket public access check + // return AccessDenied for now + { + Action: "UploadPartCopy", + Call: func(ctx context.Context) error { + _, err := s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{ + Bucket: &bucket, + Key: &obj, + UploadId: mp.UploadId, + PartNumber: &partNumber, + CopySource: getPtr("source-bucket/source-key")}) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "CompleteMultipartUpload", + Call: func(ctx context.Context) error { + _, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + UploadId: mp.UploadId, + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: []types.CompletedPart{ + { + ETag: etag, + PartNumber: &partNumber, + }, + }, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "PutObject", + Call: func(ctx context.Context) error { + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "HeadObject", + Call: func(ctx context.Context) error { + _, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &obj}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: &obj}) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAcl(ctx, &s3.GetObjectAclInput{ + Bucket: &bucket, + Key: &obj, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrNotImplemented), + }, + { + Action: "GetObjectAttributes", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: &obj, + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesEtag, + }, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "CopyObject", + Call: func(ctx context.Context) error { + _, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: &bucket, + Key: getPtr("copy-key"), + CopySource: getPtr("bucket-name/object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousCopyObject), + }, + { + Action: "ListObjects", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjects(ctx, &s3.ListObjectsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "ListObjectsV2", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "DeleteObject", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + return err + }, + ExpectedErr: nil, + }, + // FIXME: should be fixed with https://github.com/versity/versitygw/issues/1327 + { + Action: "DeleteObjects", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: &bucket, + Delete: &types.Delete{ + Objects: []types.ObjectIdentifier{ + {Key: getPtr("object-key")}, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectAcl", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectAcl(ctx, &s3.PutObjectAclInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ACL: types.ObjectCannedACLPublicRead, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "ListObjectVersions", + Call: func(ctx context.Context) error { + _, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "RestoreObject", + Call: func(ctx context.Context) error { + _, err := s3client.RestoreObject(ctx, &s3.RestoreObjectInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + RestoreRequest: &types.RestoreRequest{ + Days: aws.Int32(1), + GlacierJobParameters: &types.GlacierJobParameters{ + Tier: types.TierStandard, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "SelectObjectContent", + Call: func(ctx context.Context) error { + _, err := s3client.SelectObjectContent(ctx, &s3.SelectObjectContentInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + ExpressionType: types.ExpressionTypeSql, + Expression: getPtr("SELECT * FROM S3Object"), + InputSerialization: &types.InputSerialization{ + CSV: &types.CSVInput{}, + }, + OutputSerialization: &types.OutputSerialization{ + CSV: &types.CSVOutput{}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousRequest), + }, + { + Action: "GetBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutBucketTagging(ctx, &s3.PutBucketTaggingInput{ + Bucket: &bucket, + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteBucketTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteBucketTagging(ctx, &s3.DeleteBucketTaggingInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "DeleteObjectTagging", + Call: func(ctx context.Context) error { + _, err := s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Days: aws.Int32(1), + Mode: types.ObjectLockRetentionModeCompliance, + }, + }, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectLockConfiguration", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{ + Bucket: &bucket, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: aws.Time(time.Now().Add(24 * time.Hour)), + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectRetention", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "PutObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + { + Action: "GetObjectLegalHold", + Call: func(ctx context.Context) error { + _, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: getPtr("object-key"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + err := test.Call(ctx) + cancel() + if err == nil && test.ExpectedErr != nil { + return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr) + } + if err != nil { + if test.ExpectedErr == nil { + return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err) + } + + apiErr, ok := test.ExpectedErr.(s3err.APIError) + if !ok { + return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError") + } + + // The head requests doesn't have request body, thus only the status needs to be checked + if test.Action == "HeadBucket" || test.Action == "HeadObject" { + if err := checkSdkApiErr(err, http.StatusText(apiErr.HTTPStatusCode)); err != nil { + return fmt.Errorf("%v: %w", test.Action, err) + } + continue + } + + if err := checkApiErr(err, apiErr); err != nil { + return fmt.Errorf("%v: %w", test.Action, err) + } + } + } + + return nil + }, withAnonymousClient(), withOwnership(types.ObjectOwnershipBucketOwnerPreferred)) +} + // IAM related tests // multi-user iam tests func IAM_user_access_denied(s *S3Conf) error { diff --git a/tests/integration/utils.go b/tests/integration/utils.go index f87576d..569724d 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -178,6 +178,8 @@ type setupCfg struct { LockEnabled bool VersioningStatus types.BucketVersioningStatus Ownership types.ObjectOwnership + Anonymous bool + SkipTearDown bool } type setupOpt func(*setupCfg) @@ -191,26 +193,47 @@ func withOwnership(o types.ObjectOwnership) setupOpt { func withVersioning(v types.BucketVersioningStatus) setupOpt { return func(s *setupCfg) { s.VersioningStatus = v } } +func withAnonymousClient() setupOpt { + return func(s *setupCfg) { s.Anonymous = true } +} +func withSkipTearDown() setupOpt { + return func(s *setupCfg) { s.SkipTearDown = true } +} func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error, opts ...setupOpt) error { runF(testName) bucketName := getBucketName() + + cfg := new(setupCfg) + for _, opt := range opts { + opt(cfg) + } + err := setup(s, bucketName, opts...) if err != nil { failF("%v: failed to create a bucket: %v", testName, err) return fmt.Errorf("%v: failed to create a bucket: %w", testName, err) } - client := s.GetClient() + + var client *s3.Client + if cfg.Anonymous { + client = s.GetAnonymousClient() + } else { + client = s.GetClient() + } + handlerErr := handler(client, bucketName) if handlerErr != nil { failF("%v: %v", testName, handlerErr) } - err = teardown(s, bucketName) - if err != nil { - fmt.Printf(colorRed+"%v: failed to delete the bucket: %v", testName, err) - if handlerErr == nil { - return fmt.Errorf("%v: failed to delete the bucket: %w", testName, err) + if !cfg.SkipTearDown { + err = teardown(s, bucketName) + if err != nil { + fmt.Printf(colorRed+"%v: failed to delete the bucket: %v", testName, err) + if handlerErr == nil { + return fmt.Errorf("%v: failed to delete the bucket: %w", testName, err) + } } } if handlerErr == nil { @@ -976,6 +999,52 @@ func genPolicyDoc(effect, principal, action, resource string) string { return fmt.Sprintf(jsonTemplate, effect, principal, action, resource) } +type policyType string + +const ( + policyTypeBucket policyType = "bucket" + policyTypeObject policyType = "object" + policyTypeFull policyType = "full" +) + +func grantPublicBucketPolicy(client *s3.Client, bucket string, tp policyType) error { + var doc string + + switch tp { + case policyTypeBucket: + doc = genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%s"`, bucket)) + case policyTypeObject: + doc = genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)) + case policyTypeFull: + template := ` + { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:*", + "Resource": "arn:aws:s3:::%s" + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:*", + "Resource": "arn:aws:s3:::%s/*" + } + ] + } + ` + doc = fmt.Sprintf(template, bucket, bucket) + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{ + Bucket: &bucket, + Policy: &doc, + }) + cancel() + return err +} + func getMalformedPolicyError(msg string) s3err.APIError { return s3err.APIError{ Code: "MalformedPolicy", @@ -1334,3 +1403,9 @@ func checkObjectMetaProps(client *s3.Client, bucket, object string, o ObjectMeta func getBoolPtr(b bool) *bool { return &b } + +type PublicBucketTestCase struct { + Action string + Call func(ctx context.Context) error + ExpectedErr error +}