Files
versitygw/tests/integration/PutBucketPolicy.go
T
niksis02 23d8384ec0 feat: add s3:if-match and s3:if-none-match policy condition keys
Closes #2294

Support AWS's conditional-write condition keys in both S3 bucket policies and IAM identity policies, so a policy can require that an upload or delete carry an `If-Match`/`If-None-Match` precondition.

`requestConditionContext` now populates both keys from the request headers, which covers both policy types at once: the same map is evaluated in-process for bucket policies and shipped to the IAM service for identity policies. Only the three requests whose preconditions the gateway actually enforces contribute - PutObject, CompleteMultipartUpload and DeleteObject. Copies, form uploads, DeleteObjects batches, upload parts and the sub-resource writes all ignore the headers, and a policy must never grant on a precondition that won't be checked; reads are excluded for the same reason, since GET and HEAD take these headers as ordinary HTTP cache preconditions. The value is the ETag with its surrounding quotes stripped, matching what the gateway enforces against.

Bucket policies validate condition keys against a fixed catalogue at `PutBucketPolicy` time, so both keys are added there with the action sets AWS accepts: `s3:if-match` on `s3:PutObject` and `s3:DeleteObject`, `s3:if-none-match` on `s3:PutObject` alone. Identity policies validate only the operator vocabulary, matching AWS, so they need no change.
2026-09-09 17:18:42 +04:00

910 lines
30 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package integration
import (
"context"
"fmt"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/versity/versitygw/s3err"
)
func PutBucketPolicy_non_existing_bucket(s *S3Conf) error {
testName := "PutBucketPolicy_non_existing_bucket"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
doc := genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%v"`, bucket))
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: getPtr("non-existing-bucket"),
Policy: &doc,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_invalid_json(s *S3Conf) error {
testName := "PutBucketPolicy_invalid_json"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, doc := range []string{
"{true}",
"{asdfsdaf",
`{"Principal": "*" `,
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("This policy contains invalid Json")); err != nil {
return err
}
}
for _, doc := range []string{
"false",
"invalid_json",
"bucketPolicy",
`"Statement": []}`,
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policies must be valid JSON and the first byte must be '{'")); err != nil {
return err
}
}
return nil
})
}
func PutBucketPolicy_statement_not_provided(s *S3Conf) error {
testName := "PutBucketPolicy_statement_not_provided"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := `{}`
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Missing required field Statement")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_empty_statement(s *S3Conf) error {
testName := "PutBucketPolicy_empty_statement"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := `{"Statement": []}`
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Could not parse the policy: Statement is empty!")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_invalid_effect(s *S3Conf) error {
testName := "PutBucketPolicy_invalid_effect"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("invalid_effect", `"*"`, `"s3:*"`, `"arn:aws:s3:::*"`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid effect: invalid_effect")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_invalid_action(s *S3Conf) error {
testName := "PutBucketPolicy_invalid_action"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
for _, action := range []string{
// empty actions
`""`, "[]",
// completely invalid action
`"invalid_action"`, `["invalid_action"]`,
// only prefix
`"s3"`, `"s3:"`,
// malformed prefix
`"s4:GetObject"`, `"ss3:ListBucket"`, `"s3x:PutBucketAcl"`, `":GetObject"`, `"s3GetObject"`,
// bad separator
`"s3::GetObject"`, `"s3:Put-Object"`, `"s3:GetObject:"`, `"s3:Put(Object)"`,
// wildcard abuse
`"s3:*Obj??ect*"`, `"s3:????"`, `"s3:*:"`, `"*GetObject"`, `"???PutObject"`, `"s3:Abort?"`, `"s3:??Abort*"`,
} {
doc := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), action, fmt.Sprintf(`"arn:aws:s3:::%s"`, bucket))
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policy has invalid action")); err != nil {
return err
}
}
return nil
})
}
func PutBucketPolicy_empty_principals_string(s *S3Conf) error {
testName := "PutBucketPolicy_empty_principals_string"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `""`, `"s3:*"`, `"arn:aws:s3:::*"`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid principal in policy")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_empty_principals_array(s *S3Conf) error {
testName := "PutBucketPolicy_empty_principals_array"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `[]`, `"s3:*"`, `"arn:aws:s3:::*"`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid principal in policy")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_principals_aws_struct_empty_string(s *S3Conf) error {
testName := "PutBucketPolicy_principals_aws_struct_empty_string"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `{"AWS": ""}`, `"s3:*"`, `"arn:aws:s3:::*"`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid principal in policy")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_principals_aws_struct_empty_string_slice(s *S3Conf) error {
testName := "PutBucketPolicy_principals_aws_struct_empty_string_slice"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `{"AWS": []}`, `"s3:*"`, `"arn:aws:s3:::*"`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid principal in policy")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_principals_incorrect_wildcard_usage(s *S3Conf) error {
testName := "PutBucketPolicy_principals_incorrect_wildcard_usage"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `["*", "grt1"]`, `"s3:*"`, `"arn:aws:s3:::*"`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid principal in policy")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_non_existing_principals(s *S3Conf) error {
testName := "PutBucketPolicy_non_existing_principals"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `["a_rarely_existing_user_account_1", "a_rarely_existing_user_account_2"]`, `"s3:*"`, `"arn:aws:s3:::*"`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid principal in policy")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_empty_resources_string(s *S3Conf) error {
testName := "PutBucketPolicy_empty_resources_string"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `["*"]`, `"s3:*"`, `""`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policy has invalid resource")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_empty_resources_array(s *S3Conf) error {
testName := "PutBucketPolicy_empty_resources_array"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `["*"]`, `"s3:*"`, `[]`)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policy has invalid resource")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_invalid_resource_prefix(s *S3Conf) error {
testName := "PutBucketPolicy_invalid_resource_prefix"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
resource := fmt.Sprintf(`"arn:aws:iam:::%v"`, bucket)
doc := genPolicyDoc("Allow", `["*"]`, `"s3:*"`, resource)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policy has invalid resource")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_invalid_resource_with_starting_slash(s *S3Conf) error {
testName := "PutBucketPolicy_invalid_resource_with_starting_slash"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
resource := fmt.Sprintf(`"arn:aws:s3:::/%v"`, bucket)
doc := genPolicyDoc("Allow", `["*"]`, `"s3:*"`, resource)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policy has invalid resource")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_duplicate_resource(s *S3Conf) error {
testName := "PutBucketPolicy_duplicate_resource"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
resource := fmt.Sprintf(`"arn:aws:s3:::%v"`, bucket)
doc := genPolicyDoc("Allow", `["*"]`, `"s3:*"`, fmt.Sprintf("[%v, %v]", resource, resource))
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_incorrect_bucket_name(s *S3Conf) error {
testName := "PutBucketPolicy_incorrect_bucket_name"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
resource := fmt.Sprintf(`"arn:aws:s3:::prefix-%v"`, bucket)
doc := genPolicyDoc("Allow", `["*"]`, `"s3:*"`, resource)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policy has invalid resource")); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_action_resource_mismatch(s *S3Conf) error {
testName := "PutBucketPolicy_action_resource_mismatch"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
bucketResource := fmt.Sprintf(`"arn:aws:s3:::%s"`, bucket)
objectResource := fmt.Sprintf(`"arn:aws:s3:::%v/*"`, bucket)
for _, test := range []struct {
resource string
action string
}{
// bucket resources
{bucketResource, `"s3:GetObject"`},
{bucketResource, `"s3:PutObjectTagging"`},
{bucketResource, `"s3:GetObjec?"`},
{bucketResource, `"s3:Abort*"`},
{bucketResource, `"s3:*Multipart*"`},
{bucketResource, `"s3:???Object"`},
// object resources
{objectResource, `"s3:ListBucket"`},
{objectResource, `"s3:GetBucketTagging"`},
{objectResource, `"s3:???BucketVersioning"`},
{objectResource, `"s3:*Bucket*"`},
{objectResource, `"s3:GetBucket*"`},
} {
doc := genPolicyDoc("Allow", `["*"]`, test.action, test.resource)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Action does not apply to any resource(s) in statement")); err != nil {
return err
}
}
return nil
})
}
func PutBucketPolicy_explicit_deny(s *S3Conf) error {
testName := "PutBucketPolicy_explicit_deny"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser1, testuser2 := getUser("user"), getUser("user")
err := createUsers(s, []user{testuser1, testuser2})
if err != nil {
return err
}
resource := fmt.Sprintf("arn:aws:s3:::%v", bucket)
resourceWildCard := fmt.Sprintf("%v/*", resource)
resourcePrefix := fmt.Sprintf("%v/someprefix/*", resource)
policy := fmt.Sprintf(`{
"Statement": [
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Principal": [
"%s"
],
"Resource": [
"%v",
"%v"
]
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Principal": [
"%s"
],
"Resource": [
"%v",
"%v"
]
},
{
"Action": [
"s3:*"
],
"Effect": "Deny",
"Principal": [
"%s"
],
"Resource": "%v"
}
]
}`, testuser1.access, resourcePrefix, resource, testuser2.access, resourceWildCard, resource, testuser2.access, resourcePrefix)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &policy,
})
cancel()
if err != nil {
return err
}
userClient := s.getUserClient(testuser2)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: getPtr("someprefix/hello"),
})
cancel()
if err := checkApiErr(err, s3err.GetExplicitDenyAccessErr(
testuser2.access, "s3:PutObject", fmt.Sprintf("%v/someprefix/hello", resource), "a resource-based policy",
)); err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_multi_wildcard_resource(s *S3Conf) error {
testName := "PutBucketPolicy_multi_wildcard_resource"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
resource := fmt.Sprintf(`["arn:aws:s3:::%v/*/*", "arn:aws:s3:::%v"]`, bucket, bucket)
principal := fmt.Sprintf("\"%v\"", testuser.access)
doc := genPolicyDoc("Allow", principal, `"s3:*"`, resource)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
_, err = putObjects(userClient, []string{"foo"}, bucket)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
_, err = putObjects(userClient, []string{"bar/quxx", "foo/bar/baz", "foo/bar/xyz/quxx"}, bucket)
if err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_any_char_match(s *S3Conf) error {
testName := "PutBucketPolicy_any_char_match"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
resource := fmt.Sprintf(`["arn:aws:s3:::%v/m?-obj/*"]`, bucket)
principal := fmt.Sprintf("\"%v\"", testuser.access)
doc := genPolicyDoc("Allow", principal, `"s3:*"`, resource)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
_, err = putObjects(userClient, []string{"myy-obj/hello", "rand/foo", "my-objj/bar"}, bucket)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
_, err = putObjects(userClient, []string{"my-obj/hello", "mk-obj/foo", "m--obj/bar"}, bucket)
if err != nil {
return err
}
return nil
})
}
func PutBucketPolicy_version(s *S3Conf) error {
testName := "PutBucketPolicy_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
invalidVersionErr := getMalformedPolicyError("The policy must contain a valid version string")
for i, test := range []struct {
version string
err error
}{
{"2008-10-17", nil},
{"2012-10-17", nil},
{"", invalidVersionErr},
{"invalid", invalidVersionErr},
{"2000-10-17", invalidVersionErr},
{"2012-10-16", invalidVersionErr},
} {
policy := fmt.Sprintf(
`{
"Version": "%s",
"Statement": [
{
"Effect": "Deny",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::%s/obj"
}
]
}
`, test.version, s.awsID, bucket)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &policy,
})
cancel()
if test.err == nil && err != nil {
return fmt.Errorf("test %v failed: expected no error but got %v", i+1, err)
}
if test.err != nil {
apiErr, ok := test.err.(s3err.APIError)
if !ok {
return fmt.Errorf("test %v failed: expected s3err.APIError", i+1)
}
if err := checkApiErr(err, apiErr); err != nil {
return fmt.Errorf("test %v failed: %v", i+1, err)
}
}
}
return nil
})
}
func PutBucketPolicy_success(s *S3Conf) error {
testName := "PutBucketPolicy_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser1, testuser2 := getUser("user"), getUser("user")
err := createUsers(s, []user{testuser1, testuser2})
if err != nil {
return err
}
bucketResource := fmt.Sprintf(`"arn:aws:s3:::%v"`, bucket)
objectResource := fmt.Sprintf(`"arn:aws:s3:::%v/*"`, bucket)
for _, doc := range []string{
genPolicyDoc("Allow", fmt.Sprintf(`["%s", "%s"]`, testuser1.access, testuser2.access), `["s3:DeleteBucket", "s3:GetBucketAcl"]`, bucketResource),
genPolicyDoc("Allow", fmt.Sprintf(`{"AWS": ["%s", "%s"]}`, testuser1.access, testuser2.access), `["s3:DeleteBucket", "s3:GetBucketAcl"]`, bucketResource),
genPolicyDoc("Deny", `"*"`, `"s3:DeleteBucket"`, fmt.Sprintf(`"arn:aws:s3:::%v"`, bucket)),
genPolicyDoc("Deny", `{"AWS": "*"}`, `"s3:DeleteBucket"`, fmt.Sprintf(`"arn:aws:s3:::%v"`, bucket)),
genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser1.access), `["s3:PutBucketVersioning", "s3:ListMultipartUploadParts", "s3:ListBucket"]`, fmt.Sprintf(`[%v, %v]`, bucketResource, objectResource)),
genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`[%v, %v]`, bucketResource, objectResource)),
genPolicyDoc("Allow", `"*"`, `"s3:Get*"`, objectResource),
genPolicyDoc("Deny", `"*"`, `"s3:Create*"`, fmt.Sprintf(`[%v, %v]`, bucketResource, objectResource)),
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
}
return nil
})
}
func PutBucketPolicy_status(s *S3Conf) error {
testname := "PutBucketPolicy_status"
return actionHandler(s, testname, func(s3client *s3.Client, bucket string) error {
doc := genPolicyDoc("Allow", `"*"`, `"s3:GetObject"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"?policy", s.awsID, s.awsSecret, "s3", s.awsRegion, "", []byte(doc), time.Now(), nil)
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("expected the response status code to be %v, instead got %v", http.StatusNoContent, resp.StatusCode)
}
return nil
})
}
func PutBucketPolicy_condition_invalid_operator(s *S3Conf) error {
testName := "PutBucketPolicy_condition_invalid_operator"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, tc := range []struct {
condition string
// operatorName is the exact, as-written operator name AWS's
// "Invalid Condition type : <Name>" message echoes back.
operatorName string
}{
// completely unrecognized operator
{`{"NotARealOperator":{"aws:SourceIp":"1.2.3.4/32"}}`, "NotARealOperator"},
// operator names are case-sensitive - lowercase is unrecognized
{`{"stringequals":{"aws:UserAgent":"foo"}}`, "stringequals"},
// IfExists suffix on an operator that doesn't take one
{`{"NullIfExists":{"aws:UserAgent":"true"}}`, "NullIfExists"},
// unrecognized ForAllValues/ForAnyValue qualifier prefix
{`{"ForSomeValues:StringEquals":{"aws:UserAgent":"foo"}}`, "ForSomeValues:StringEquals"},
} {
doc := fmt.Sprintf(`{"Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject",
"Resource":"arn:aws:s3:::%s/*","Condition":%s}]}`, bucket, tc.condition)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError(fmt.Sprintf("Invalid Condition type : %s", tc.operatorName))); err != nil {
return err
}
}
return nil
})
}
func PutBucketPolicy_condition_invalid_key(s *S3Conf) error {
testName := "PutBucketPolicy_condition_invalid_key"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, condition := range []string{
// unrecognized key under a String operator
`{"StringEquals":{"s3:FakeKeyDoesNotExist":"foo"}}`,
// unrecognized key under a Numeric operator
`{"NumericEquals":{"s3:NotARealKey":"5"}}`,
// unrecognized key under IpAddress
`{"IpAddress":{"aws:NotARealIpKey":"10.0.0.0/8"}}`,
// a plausible-looking but non-existent aws: global key
`{"StringEquals":{"aws:NotARealGlobalKey":"foo"}}`,
} {
doc := fmt.Sprintf(`{"Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject",
"Resource":"arn:aws:s3:::%s/*","Condition":%s}]}`, bucket, condition)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Policy has an invalid condition key")); err != nil {
return err
}
}
return nil
})
}
func PutBucketPolicy_condition_action_mismatch(s *S3Conf) error {
testName := "PutBucketPolicy_condition_action_mismatch"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, tc := range []struct {
action string
condition string
}{
// s3:prefix only applies to s3:ListBucket/s3:ListBucketVersions,
// not s3:GetObject.
{`"s3:GetObject"`, `{"StringEquals":{"s3:prefix":"foo"}}`},
// ... and notably not s3:ListBucketMultipartUploads either,
// despite also being a List-shaped action.
{`"s3:ListBucketMultipartUploads"`, `{"StringEquals":{"s3:prefix":"foo"}}`},
// s3:x-amz-acl only applies to s3:PutObject/PutBucketAcl/PutObjectAcl.
{`"s3:GetObject"`, `{"StringEquals":{"s3:x-amz-acl":"public-read"}}`},
// s3:VersionId only applies to the *Version* action family.
{`"s3:GetObject"`, `{"StringEquals":{"s3:VersionId":"abc123"}}`},
// an explicit multi-action list requires every action to
// support the key, even though s3:PutObject alone would.
{`["s3:GetObject","s3:PutObject"]`, `{"StringEquals":{"s3:x-amz-acl":"public-read"}}`},
// s3:if-match applies to the conditional-write actions only,
// so a read action is rejected...
{`"s3:GetObject"`, `{"StringEquals":{"s3:if-match":"abc123"}}`},
// ... and so is a versioned delete, which names the version to
// remove rather than taking an If-Match.
{`"s3:DeleteObjectVersion"`, `{"StringEquals":{"s3:if-match":"abc123"}}`},
// ... as is a bucket-level write, which reads no If-Match at
// all.
{`"s3:PutBucketVersioning"`, `{"StringEquals":{"s3:if-match":"abc123"}}`},
// s3:if-none-match is narrower still: only s3:PutObject can
// require that the object not already exist.
{`"s3:DeleteObject"`, `{"Null":{"s3:if-none-match":"false"}}`},
{`["s3:PutObject","s3:DeleteObject"]`, `{"Null":{"s3:if-none-match":"false"}}`},
} {
doc := fmt.Sprintf(`{"Statement":[{"Effect":"Allow","Principal":"*","Action":%s,
"Resource":"arn:aws:s3:::%s/*","Condition":%s}]}`, tc.action, bucket, tc.condition)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Conditions do not apply to combination of actions and resources in statement")); err != nil {
return err
}
}
return nil
})
}
func PutBucketPolicy_condition_conditional_write_keys(s *S3Conf) error {
testName := "PutBucketPolicy_condition_conditional_write_keys"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, tc := range []struct {
action string
condition string
}{
{`"s3:PutObject"`, `{"StringEquals":{"s3:if-match":"abc123"}}`},
{`"s3:PutObject"`, `{"Null":{"s3:if-none-match":"false"}}`},
// s3:if-match also covers S3's conditional delete.
{`"s3:DeleteObject"`, `{"StringEquals":{"s3:if-match":"abc123"}}`},
{`["s3:PutObject","s3:DeleteObject"]`, `{"StringEquals":{"s3:if-match":"abc123"}}`},
// a wildcard action is exempt from the applicability check
{`"s3:*"`, `{"Null":{"s3:if-none-match":"true"}}`},
// key names are case-insensitive
{`"s3:PutObject"`, `{"StringEquals":{"S3:IF-MATCH":"abc123"}}`},
{`"s3:PutObject"`, `{"Null":{"s3:If-None-Match":"false"}}`},
// both keys in one statement
{`"s3:PutObject"`, `{"Null":{"s3:if-match":"true","s3:if-none-match":"false"}}`},
} {
doc := fmt.Sprintf(`{"Statement":[{"Effect":"Allow","Principal":"*","Action":%s,
"Resource":"arn:aws:s3:::%s/*","Condition":%s}]}`, tc.action, bucket, tc.condition)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return fmt.Errorf("action %s with condition %s: %w", tc.action, tc.condition, err)
}
}
return nil
})
}
func PutBucketPolicy_condition_invalid_ip(s *S3Conf) error {
testName := "PutBucketPolicy_condition_invalid_ip"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, condition := range []string{
// not an IP address at all
`{"IpAddress":{"aws:SourceIp":"not-an-ip"}}`,
// malformed CIDR notation (octet out of range, bad prefix length)
`{"IpAddress":{"aws:SourceIp":"300.1.1.1/40"}}`,
// the same bad value under NotIpAddress
`{"NotIpAddress":{"aws:SourceIp":"not-an-ip"}}`,
// the IP-format check fires regardless of which operator wraps
// the key - even Null, which has nothing to do with IP syntax.
`{"Null":{"aws:SourceIp":"true"}}`,
} {
doc := fmt.Sprintf(`{"Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject",
"Resource":"arn:aws:s3:::%s/*","Condition":%s}]}`, bucket, condition)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err := checkApiErr(err, getMalformedPolicyError("Invalid IP address in Conditions")); err != nil {
return err
}
}
return nil
})
}