Files
versitygw/tests/integration/Access_Control.go
niksis02 cd0b4e6d9d fix: normalize object keys during bucket policy evaluation
Object key validation allowed internal parent-directory segments such as `public/../private.txt`. Bucket policy and auth checks evaluated the raw key, so a policy allowing bucket/public/* could match the request while posix backend later resolved the key with `filepath.Join` and accessed `bucket/private.txt`.

Add backend-specific object key normalization to close that mismatch. The Backend interface now exposes `NormalizeObjectKey` so authorization can evaluate resources using the same key shape a backend will use for storage access.

Backends that do not collapse object paths, including Azure and the S3 proxy, inherit `BackendUnsupported.NormalizeObjectKey`. That implementation returns the input key unchanged, avoiding unnecessary normalization and keeping policy evaluation unpolluted for object stores where ../ is part of the key name.

posix/scoutfs normalize keys with filepath.Join so policy resources and request keys are compared after internal dot segments are collapsed.

Bucket policy evaluation now normalizes both the incoming object key and object resource patterns from the policy before matching. Object lock governance bypass policy checks use the same backend normalizer as well, so retention and legal hold authorization cannot diverge from backend path resolution.
2026-05-27 22:20:39 +04:00

1113 lines
33 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 (
"bytes"
"context"
"fmt"
"io"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
// Access control tests (with bucket ACLs and Policies)
func AccessControl_default_ACL_user_access_denied(s *S3Conf) error {
testName := "AccessControl_default_ACL_user_access_denied"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
_, err = putObjects(userClient, []string{"my-obj"}, bucket)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
return nil
})
}
func AccessControl_default_ACL_userplus_access_denied(s *S3Conf) error {
testName := "AccessControl_default_ACL_userplus_access_denied"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("userplus")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
client := s.getUserClient(testuser)
_, err = putObjects(client, []string{"my-obj"}, bucket)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
return nil
})
}
func AccessControl_default_ACL_admin_successful_access(s *S3Conf) error {
testName := "AccessControl_default_ACL_admin_successful_access"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("admin")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
adminClient := s.getUserClient(testuser)
_, err = putObjects(adminClient, []string{"my-obj"}, bucket)
if err != nil {
return err
}
return nil
})
}
func AccessControl_bucket_resource_single_action(s *S3Conf) error {
testName := "AccessControl_bucket_resource_single_action"
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
}
doc := genPolicyDoc("Allow", fmt.Sprintf(`["%s"]`, testuser1.access), `"s3:PutBucketTagging"`, fmt.Sprintf(`"arn:aws:s3:::%v"`, bucket))
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
testuser1Client := s.getUserClient(testuser1)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = testuser1Client.DeleteBucketTagging(ctx, &s3.DeleteBucketTaggingInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = testuser1Client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{
Bucket: &bucket,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
user2Client := s.getUserClient(testuser2)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = user2Client.DeleteBucketTagging(ctx, &s3.DeleteBucketTaggingInput{
Bucket: &bucket,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
return nil
})
}
func AccessControl_bucket_resource_all_action(s *S3Conf) error {
testName := "AccessControl_bucket_resource_all_action"
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)
doc := genPolicyDoc("Allow", fmt.Sprintf(`["%s"]`, testuser1.access), `"s3:*"`, 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
}
testuser1Client := s.getUserClient(testuser1)
_, err = putObjects(testuser1Client, []string{"my-obj"}, bucket)
if err != nil {
return err
}
user2Client := s.getUserClient(testuser2)
_, err = putObjects(user2Client, []string{"my-obj"}, bucket)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
return nil
})
}
func AccessControl_single_object_resource_actions(s *S3Conf) error {
testName := "AccessControl_single_object_resource_actions"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj/nested-obj"
_, err := putObjects(s3client, []string{obj}, bucket)
if err != nil {
return err
}
testuser := getUser("user")
err = createUsers(s, []user{testuser})
if err != nil {
return err
}
doc := genPolicyDoc("Allow", fmt.Sprintf(`["%s"]`, testuser.access), `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%v/%v"`, bucket, obj))
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
testuser1Client := s.getUserClient(testuser)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = testuser1Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = testuser1Client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{
Bucket: &bucket,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
return nil
})
}
func AccessControl_multi_statement_policy(s *S3Conf) error {
testName := "AccessControl_multi_statement_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
policy := fmt.Sprintf(`{
"Statement": [
{
"Effect": "Deny",
"Principal": ["%s"],
"Action": "s3:DeleteBucket",
"Resource": "arn:aws:s3:::%s"
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::%s", "arn:aws:s3:::%s/*"]
}
]
}`, testuser.access, bucket, testuser.access, bucket, bucket)
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(testuser)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.ListObjects(ctx, &s3.ListObjectsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.DeleteBucket(ctx, &s3.DeleteBucketInput{
Bucket: &bucket,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
return nil
})
}
func AccessControl_bucket_ownership_to_user(s *S3Conf) error {
testName := "AccessControl_bucket_ownership_to_user"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
if err := changeBucketsOwner(s, []string{bucket}, testuser.access); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.HeadBucket(ctx, &s3.HeadBucketInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
return nil
})
}
func AccessControl_root_PutBucketAcl(s *S3Conf) error {
testName := "AccessControl_root_PutBucketAcl"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
if err := changeBucketsOwner(s, []string{bucket}, testuser.access); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.PutBucketAcl(ctx, &s3.PutBucketAclInput{
Bucket: &bucket,
ACL: types.BucketCannedACLPrivate,
})
cancel()
if err != nil {
return err
}
return nil
}, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func AccessControl_user_PutBucketAcl_with_policy_access(s *S3Conf) error {
testName := "AccessControl_user_PutBucketAcl_with_policy_access"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%v"`, testuser.access), `"s3:PutBucketAcl"`, fmt.Sprintf(`"arn:aws:s3:::%v"`, bucket))
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(testuser)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.PutBucketAcl(ctx, &s3.PutBucketAclInput{
Bucket: &bucket,
ACL: types.BucketCannedACLPublicRead,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
expectedGrants := []types.Grant{
{
Grantee: &types.Grantee{
ID: &s.awsID,
Type: types.TypeCanonicalUser,
},
Permission: types.PermissionFullControl,
},
{
Grantee: &types.Grantee{
ID: getPtr("all-users"),
Type: types.TypeGroup,
},
Permission: types.PermissionRead,
},
}
if !compareGrants(res.Grants, expectedGrants) {
return fmt.Errorf("expected the resulting grants to be %v, instead got %v",
expectedGrants, res.Grants)
}
return nil
}, withOwnership(types.ObjectOwnershipBucketOwnerPreferred))
}
func AccessControl_copy_object_with_starting_slash_for_user(s *S3Conf) error {
testName := "AccessControl_copy_object_with_starting_slash_for_user"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := putObjects(s3client, []string{obj}, bucket)
if err != nil {
return err
}
testuser := getUser("user")
err = createUsers(s, []user{testuser})
if err != nil {
return err
}
if err := changeBucketsOwner(s, []string{bucket}, testuser.access); err != nil {
return err
}
copySource := fmt.Sprintf("/%v/%v", bucket, obj)
meta := map[string]string{
"key1": "val1",
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: &copySource,
Metadata: meta,
MetadataDirective: types.MetadataDirectiveReplace,
})
cancel()
if err != nil {
return err
}
return nil
})
}
func AccessControl_policy_normalizes_object_key_for_get_put_delete(s *S3Conf) error {
testName := "AccessControl_policy_normalizes_object_key_for_get_put_delete"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
privateKey := "private.txt"
traversalKey := "public/../" + privateKey
privateBody := []byte("private object body")
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &privateKey,
Body: bytes.NewReader(privateBody),
}, s3client)
if err != nil {
return err
}
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access),
`["s3:GetObject","s3:PutObject","s3:DeleteObject"]`,
fmt.Sprintf(`"arn:aws:s3:::%s/public/*"`, bucket))
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &traversalKey,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
copySource := fmt.Sprintf("%s/%s", bucket, traversalKey)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: getPtr("public/copied.txt"),
CopySource: &copySource,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
_, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &traversalKey,
Body: bytes.NewReader([]byte("overwrite")),
}, userClient)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &privateKey,
})
cancel()
if err != nil {
return err
}
defer out.Body.Close()
gotBody, err := io.ReadAll(out.Body)
if err != nil {
return fmt.Errorf("read private object body: %w", err)
}
if !bytes.Equal(gotBody, privateBody) {
return fmt.Errorf("expected private object body to remain %q, instead got %q", privateBody, gotBody)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &traversalKey,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &privateKey,
})
cancel()
return err
})
}
func AccessControl_PutObject_with_tagging_policy(s *S3Conf) error {
testName := "AccessControl_PutObject_with_tagging_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectTagging
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
tagging := "key=value"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
}, userClient)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectTagging
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectTagging"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
_, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
}, userClient)
return err
})
}
func AccessControl_PutObject_with_legal_hold_policy(s *S3Conf) error {
testName := "AccessControl_PutObject_with_legal_hold_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectLegalHold
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
}, userClient)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectLegalHold
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectLegalHold"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
_, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
}, userClient)
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: obj, removeLegalHold: true}})
}, withLock())
}
func AccessControl_PutObject_with_retention_policy(s *S3Conf) error {
testName := "AccessControl_PutObject_with_retention_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
date := time.Now().Add(time.Hour)
// Error path: user has s3:PutObject but not s3:PutObjectRetention
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
}, userClient)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectRetention
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectRetention"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
_, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
}, userClient)
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: obj}})
}, withLock())
}
func AccessControl_CreateMultipartUpload_with_tagging_policy(s *S3Conf) error {
testName := "AccessControl_CreateMultipartUpload_with_tagging_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectTagging
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
tagging := "key=value"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectTagging
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectTagging"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
})
cancel()
return err
})
}
func AccessControl_CreateMultipartUpload_with_legal_hold_policy(s *S3Conf) error {
testName := "AccessControl_CreateMultipartUpload_with_legal_hold_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectLegalHold
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectLegalHold
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectLegalHold"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
return err
}, withLock())
}
func AccessControl_CreateMultipartUpload_with_retention_policy(s *S3Conf) error {
testName := "AccessControl_CreateMultipartUpload_with_retention_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
date := time.Now().Add(time.Hour)
// Error path: user has s3:PutObject but not s3:PutObjectRetention
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectRetention
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectRetention"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
})
cancel()
return err
}, withLock())
}
func AccessControl_CopyObject_with_tagging_policy(s *S3Conf) error {
testName := "AccessControl_CopyObject_with_tagging_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
dstObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, dstObj)
srcObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, srcObj)
// Error path: user has s3:PutObject, s3:GetObject but not s3:PutObjectTagging
policy := fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:PutObject",
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
Tagging: getPtr("key=value"),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:GetObject, s3:PutObject and s3:PutObjectTagging
policy = fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": ["s3:PutObject","s3:PutObjectTagging"],
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
Tagging: getPtr("key=value"),
})
cancel()
return err
})
}
func AccessControl_CopyObject_with_legal_hold_policy(s *S3Conf) error {
testName := "AccessControl_CopyObject_with_legal_hold_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
dstObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, dstObj)
srcObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, srcObj)
// Error path: user has s3:PutObject, s3:GetObject but not s3:PutObjectLegalHold
policy := fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:PutObject",
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:GetObject, s3:PutObject and s3:PutObjectLegalHold
policy = fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": ["s3:PutObject","s3:PutObjectLegalHold"],
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: dstObj, removeLegalHold: true}})
}, withLock())
}
func AccessControl_CopyObject_with_retention_policy(s *S3Conf) error {
testName := "AccessControl_CopyObject_with_retention_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
dstObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, dstObj)
srcObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, srcObj)
// Error path: user has s3:PutObject, s3:GetObject but not s3:PutObjectRetention
policy := fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:PutObject",
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: getPtr(time.Now().AddDate(1, 0, 0)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:GetObject, s3:PutObject and s3:PutObjectRetention
policy = fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": ["s3:PutObject","s3:PutObjectRetention"],
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: getPtr(time.Now().AddDate(1, 0, 0)),
})
cancel()
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: dstObj}})
}, withLock())
}