Files
threshold-plugin-aws/plugin/service/storage/plugin_test.go
T
Damian DebkowskiandHugo fcfe267a3f refactor: switch from awsutil to awsutilv2 (#34)
* fix(storage): Storage Assume Role Support

This commit introduces fixes to enable operators to use AWS dynamic
credentials on the storage side of this plugin, namely validation,
improved credential lifecycle management to allow for storage buckets to
be updated from static to dynamic credentials and vice versa, and a new
way to determine credential types.

It also introduces new tests for dynamic credential usage for the
various functions.

* fix(storage): Static rotated creds being deleted on plugin err

On Storage Bucket update, when we determine if the credentials are to be
updated to new ones, we call `credState.ReplaceCreds`.

This call, if the old credentials were managed by us (rotated at create
time), will delete the old credentials and set the new ones onto
credState.

We called this function without validating if the incoming credentials
are valid. This could get the bucket in a state where where, while we'd
return an update error if the incoming credentials weren't working with
S3 (err in the dry run process that happens at the end of the update),
we'd also silently delete the old credentials the bucket used, rendering
the bucket completely broken.

---------

Co-authored-by: Hugo <hugoamvieira@users.noreply.github.com>
2023-09-22 12:26:08 -07:00

2738 lines
86 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/aws-sdk-go-v2/service/sts/types"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)
func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
validRequest := func() *pb.OnCreateStorageBucketRequest {
return &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
}
}
cases := []struct {
name string
req *pb.OnCreateStorageBucketRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
expectedPersistedData map[string]any
}{
{
name: "nil storage bucket",
req: &pb.OnCreateStorageBucketRequest{},
expectedErrContains: "bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "empty bucket name",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
},
expectedErrContains: "attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: new(structpb.Struct),
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "dynamic credentials without disable credential rotation",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{Fields: map[string]*structpb.Value{}},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
},
},
},
},
expectedErrContains: "disable_credential_rotation attribute is required when providing a role_arn",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "rotation error",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
),
),
}),
},
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "dryRunValidation failed putObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(
nil,
testMockS3WithPutObjectError(errors.New(testPutObjectErr)),
)),
},
expectedErrContains: "error failed to put object: test error for PutObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation putObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(
nil,
testMockS3WithPutObjectError(new(throttleErr)),
)),
},
expectedErrContains: "error failed to put object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "dryRunValidation failed getObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectError(errors.New(testGetObjectErr)),
),
),
},
expectedErrContains: "error failed to get object: test error for GetObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed getObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error failed to get object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "dryRunValidation failed headObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)),
),
),
},
expectedErrContains: "error failed to get head object: test error for HeadObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed headObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error failed to get head object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "success",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: credential.MockStaticCredentialSecrets().AsMap(),
},
{
name: "success rotate static creds",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts(
[]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("one"),
SecretAccessKey: aws.String("two"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
},
),
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{
credential.ConstAccessKeyId: "one",
credential.ConstSecretAccessKey: "two",
},
},
{
name: "success with dynamic credentials",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{},
Attributes: credential.MockAssumeRoleAttributes("us-west-2", true),
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts(
[]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(
awsutil.WithAssumeRoleOutput(&sts.AssumeRoleOutput{
Credentials: &types.Credentials{
AccessKeyId: aws.String("ASIAfoobar"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SecretAccessKey: aws.String("secretkeyfoobar"),
},
}),
),
),
},
),
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{}, // No credentials sent back to Boundary as they are temporary.
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
resp, err := p.OnCreateStorageBucket(context.Background(), tc.req)
if tc.expectedErrContains != "" {
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
}
require.NoError(err)
require.NotNil(resp)
require.NotNil(resp.GetPersisted())
require.NotNil(resp.GetPersisted().GetData())
actualPersistedData := resp.GetPersisted().GetData().AsMap()
if len(tc.expectedPersistedData) != 0 {
require.NotEmpty(actualPersistedData)
require.Equal(tc.expectedPersistedData[credential.ConstAccessKeyId], actualPersistedData[credential.ConstAccessKeyId])
require.Equal(tc.expectedPersistedData[credential.ConstSecretAccessKey], actualPersistedData[credential.ConstSecretAccessKey])
require.NotEmpty(actualPersistedData[credential.ConstCredsLastRotatedTime])
}
})
}
}
// TODO: add tests for changing:
// roleARN values
// access/secret keys
// swithcing between static & dynamic credentials
func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
validRequest := func() *pb.OnUpdateStorageBucketRequest {
return &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: credential.MockStaticCredentialSecrets(),
},
}
}
cases := []struct {
name string
req *pb.OnUpdateStorageBucketRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedPersistedData map[string]any
expectCredsLastRotatedTime bool
expectedErrContains string
expectedErrCode codes.Code
}{
{
name: "nil new bucket",
req: &pb.OnUpdateStorageBucketRequest{},
expectedErrContains: "new bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "empty new bucketName",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "new bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil current bucket",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
},
expectedErrContains: "current bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil current bucket attributes",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
CurrentBucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "current bucket attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil new bucket attributes",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
expectedErrContains: "new bucket attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: new(structpb.Struct),
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "cannot disable rotation for already rotated credentials",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
expectedErrContains: "cannot disable rotation for already-rotated credentials",
expectedErrCode: codes.FailedPrecondition,
},
{
name: "attempt to enable credential rotation with dynamic credentials",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: credential.MockAssumeRoleAttributes("us-west-2", true),
},
},
expectedErrContains: "disable_credential_rotation attribute is required when providing a role_arn",
expectedErrCode: codes.InvalidArgument,
},
{
name: "updating secrets, replace error",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_onetwo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("threefour"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
}),
},
expectedErrContains: fmt.Sprintf("error attempting to replace credentials: error deleting old access key: %s", testDeleteAccessKeyErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "rotation error",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
),
),
}),
},
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "dryRunValidation failed putObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(
nil,
testMockS3WithPutObjectError(errors.New(testPutObjectErr)),
)),
},
expectedErrContains: "error failed to put object: test error for PutObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed putObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(
nil,
testMockS3WithPutObjectError(new(throttleErr)),
)),
},
expectedErrContains: "error failed to put object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "dryRunValidation failed getObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectError(errors.New(testGetObjectErr)),
),
),
},
expectedErrContains: "error failed to get object: test error for GetObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed getObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error failed to get object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "dryRunValidation failed headObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)),
),
),
},
expectedErrContains: "error failed to get head object: test error for HeadObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed headObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error failed to get head object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "success with credential rotation",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("AKIA_one"),
SecretAccessKey: aws.String("two"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
}),
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{
credential.ConstAccessKeyId: "AKIA_one",
credential.ConstSecretAccessKey: "two",
},
expectCredsLastRotatedTime: true,
},
{
name: "success",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{
credential.ConstAccessKeyId: "AKIA_foobar",
credential.ConstSecretAccessKey: "bazqux",
},
},
{
name: "dry run fail on dynamic to dynamic credentials update",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{Data: &structpb.Struct{}},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithHeadObjectError(fmt.Errorf("head object failed oops")),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
),
),
},
expectedErrContains: "head object failed oops",
expectedErrCode: codes.InvalidArgument,
},
{
name: "dynamic to dynamic credentials success",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{Data: &structpb.Struct{}},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{},
},
{
name: "dry run fail on static non rotated to dynamic credentials update",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
},
},
},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectError(fmt.Errorf("put object failed oops")),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedErrContains: "put object failed oops",
expectedErrCode: codes.InvalidArgument,
},
{
name: "success static non rotated to dynamic credentials",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
},
},
},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{},
},
{
name: "DeleteAccessKey error on static rotated to dynamic credentials update",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar_rotated_accesskeyid"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Now().Add(-time.Hour).Format(time.RFC3339Nano)),
},
},
},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(fmt.Errorf("delete access key fail oops")),
),
),
}),
},
expectedErrContains: "delete access key fail oops",
expectedErrCode: codes.InvalidArgument,
},
{
name: "success static rotated to dynamic credentials",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar_rotated_accesskeyid"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Now().Add(-time.Hour).Format(time.RFC3339Nano)),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(), // DeleteAccessKey Success
),
}),
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{},
},
{
name: "dry run fail on dynamic to static non-rotated credentials update",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{Fields: map[string]*structpb.Value{}},
},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectError(fmt.Errorf("put object fail oops")),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedErrContains: "put object fail oops",
expectedErrCode: codes.InvalidArgument,
},
{
name: "success dynamic to static non-rotated credentials",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{Fields: map[string]*structpb.Value{}},
},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{
credential.ConstAccessKeyId: "AKIA_foobar",
credential.ConstSecretAccessKey: "secretkey",
},
},
{
name: "create access key error on dynamic to static rotated credentials update",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{Fields: map[string]*structpb.Value{}},
},
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyError(fmt.Errorf("create access key fail oops")),
),
),
}),
},
expectedErrContains: "create access key fail oops",
expectedErrCode: codes.InvalidArgument,
},
{
name: "success dynamic to static rotated credentials",
req: &pb.OnUpdateStorageBucketRequest{
CurrentBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{Fields: map[string]*structpb.Value{}},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("AKIA_one"),
SecretAccessKey: aws.String("two"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
}),
},
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
expectedPersistedData: map[string]any{
credential.ConstAccessKeyId: "AKIA_one",
credential.ConstSecretAccessKey: "two",
},
expectCredsLastRotatedTime: true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
resp, err := p.OnUpdateStorageBucket(context.Background(), tc.req)
if tc.expectedErrContains != "" {
require.NotNil(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
}
require.NoError(err)
require.NotNil(resp)
require.NotNil(resp.GetPersisted())
require.NotNil(resp.GetPersisted().GetData())
actualPersistedData := resp.GetPersisted().GetData().AsMap()
if len(actualPersistedData) > 0 && len(tc.expectedPersistedData) == 0 {
t.Fatalf("test case expected no persisted data, but got %#v", actualPersistedData)
}
if len(tc.expectedPersistedData) != 0 {
require.NotEmpty(actualPersistedData)
require.Equal(tc.expectedPersistedData[credential.ConstAccessKeyId], actualPersistedData[credential.ConstAccessKeyId])
require.Equal(tc.expectedPersistedData[credential.ConstSecretAccessKey], actualPersistedData[credential.ConstSecretAccessKey])
if tc.expectCredsLastRotatedTime {
require.NotEmpty(actualPersistedData[credential.ConstCredsLastRotatedTime])
}
}
})
}
}
func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
cases := []struct {
name string
req *pb.OnDeleteStorageBucketRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
}{
{
name: "nil bucket",
req: &pb.OnDeleteStorageBucketRequest{},
expectedErrContains: "bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
Attributes: new(structpb.Struct),
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "delete error",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
}),
},
expectedErrContains: fmt.Sprintf("error removing rotated credentials during storage bucket deletion: error deleting old access key: %s", testDeleteAccessKeyErr),
expectedErrCode: codes.Aborted,
},
{
name: "success",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(nil),
),
),
}),
},
},
{
name: "success with dynamic credentials",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Persisted: &storagebuckets.StorageBucketPersisted{
Data: &structpb.Struct{Fields: map[string]*structpb.Value{}},
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
resp, err := p.OnDeleteStorageBucket(context.Background(), tc.req)
if tc.expectedErrContains != "" {
require.NotNil(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
}
require.NoError(err)
require.NotNil(resp)
})
}
}
func TestStoragePlugin_HeadObject(t *testing.T) {
validRequest := func() *pb.HeadObjectRequest {
return &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
}
}
cases := []struct {
name string
req *pb.HeadObjectRequest
contentLength int64
lastModified time.Time
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
}{
{
name: "empty object key",
req: &pb.HeadObjectRequest{},
expectedErrContains: "key is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil bucket",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
},
expectedErrContains: "bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "empty bucket name",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: new(structpb.Struct),
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),
credOpts: []credential.AwsCredentialPersistedStateOption{
func(s *credential.AwsCredentialPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "storage persisted state setup error",
req: validRequest(),
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "headObject error",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)),
),
),
},
expectedErrContains: "error getting head object from s3: test error for HeadObject",
expectedErrCode: codes.Internal,
},
{
name: "headObject throttle error",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithHeadObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error getting head object from s3: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "success",
req: validRequest(),
credOpts: validSTSMock(),
contentLength: 1024,
lastModified: createTime(t, "2006-01-02T15:04:05Z"),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{
ContentLength: 1024,
LastModified: aws.Time(createTime(t, "2006-01-02T15:04:05Z")),
}),
),
),
},
},
{
name: "success with dynamic credentials",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
credOpts: validSTSMock(),
contentLength: 1024,
lastModified: createTime(t, "2006-01-02T15:04:05Z"),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{
ContentLength: 1024,
LastModified: aws.Time(createTime(t, "2006-01-02T15:04:05Z")),
}),
),
),
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
resp, err := p.HeadObject(context.Background(), tc.req)
if tc.expectedErrContains != "" {
require.Error(err)
require.Nil(resp)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
}
require.NoError(err)
require.NotNil(resp)
require.Equal(tc.contentLength, resp.ContentLength)
require.Equal(tc.lastModified.String(), resp.LastModified.AsTime().String())
})
}
}
func TestStoragePlugin_ValidatePermissions(t *testing.T) {
validRequest := func() *pb.ValidatePermissionsRequest {
return &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
}
}
cases := []struct {
name string
req *pb.ValidatePermissionsRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
}{
{
name: "nil bucket",
req: &pb.ValidatePermissionsRequest{},
expectedErrContains: "bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "empty bucket name",
req: &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
req: &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: new(structpb.Struct),
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),
credOpts: []credential.AwsCredentialPersistedStateOption{
func(s *credential.AwsCredentialPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "storage persisted state setup error",
req: validRequest(),
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "dryRunValidation failed putObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(
nil,
testMockS3WithPutObjectError(errors.New(testPutObjectErr)),
)),
},
expectedErrContains: "error failed to put object: test error for PutObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed putObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(newTestMockS3(
nil,
testMockS3WithPutObjectError(new(throttleErr)),
)),
},
expectedErrContains: "error failed to put object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "dryRunValidation failed getObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectError(errors.New(testGetObjectErr)),
),
),
},
expectedErrContains: "error failed to get object: test error for GetObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed getObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error failed to get object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "dryRunValidation failed headObject",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)),
),
),
},
expectedErrContains: "error failed to get head object: test error for HeadObject",
expectedErrCode: codes.Internal,
},
{
name: "dryRunValidation failed headObject throttle",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error failed to get head object: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "success",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
},
{
name: "success with dynamic credentials",
req: &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
),
),
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
resp, err := p.ValidatePermissions(context.Background(), tc.req)
if tc.expectedErrContains != "" {
require.Error(err)
require.Nil(resp)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
}
require.NoError(err)
require.NotNil(resp)
})
}
}
func TestStoragePlugin_GetObject(t *testing.T) {
validRequest := func() *pb.GetObjectRequest {
return &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
}
}
cases := []struct {
name string
req *pb.GetObjectRequest
objectData []byte
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
}{
{
name: "empty object key",
req: &pb.GetObjectRequest{},
expectedErrContains: "key is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil bucket",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
},
expectedErrContains: "bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing bucket name",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: new(structpb.Struct),
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),
credOpts: []credential.AwsCredentialPersistedStateOption{
func(s *credential.AwsCredentialPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "storage persisted state setup error",
req: validRequest(),
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "getObject error",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithGetObjectError(errors.New(testGetObjectErr)),
),
),
},
expectedErrContains: "error getting object from s3: test error for GetObject",
expectedErrCode: codes.Internal,
},
{
name: "getObject throttle error",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithGetObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error getting object from s3: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "with chunk size",
req: func() *pb.GetObjectRequest {
request := validRequest()
request.ChunkSize = 16384
return request
}(),
credOpts: validSTSMock(),
objectData: []byte("test"),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{
Body: ioutil.NopCloser(bytes.NewReader([]byte("test"))),
}),
),
),
},
},
{
name: "success",
req: validRequest(),
credOpts: validSTSMock(),
objectData: []byte("test"),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{
Body: ioutil.NopCloser(bytes.NewReader([]byte("test"))),
}),
),
),
},
},
{
name: "success with dynamic credentials",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
credOpts: validSTSMock(),
objectData: []byte("test"),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{
Body: ioutil.NopCloser(bytes.NewReader([]byte("test"))),
}),
),
),
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
stream := newGetObjectStreamMock()
err := p.GetObject(tc.req, stream)
if tc.expectedErrContains != "" {
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
}
require.NoError(err)
resp, err := stream.Recv()
require.NoError(err)
require.NotNil(resp)
require.EqualValues(tc.objectData, resp.FileChunk)
})
}
}
func TestStoragePlugin_PutObject(t *testing.T) {
td := t.TempDir()
emptyFilePath := path.Join(td, "empty-file-test")
emptyFile, err := os.Create(emptyFilePath)
require.NoError(t, err)
require.NoError(t, emptyFile.Close())
validFilePath := path.Join(td, "valid-file-test")
validFile, err := os.Create(validFilePath)
require.NoError(t, err)
n, err := validFile.WriteString("CONTENT CHECK")
require.NoError(t, err)
require.Equal(t, len("CONTENT CHECK"), n)
require.NoError(t, validFile.Close())
validRequest := func() *pb.PutObjectRequest {
return &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Key: "mock-object",
Path: validFilePath,
}
}
cases := []struct {
name string
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
request *pb.PutObjectRequest
setup func()
expectedObject []byte
expectedErrContains string
expectedErrCode codes.Code
}{
{
name: "missing request",
expectedErrContains: "request is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing object key",
request: &pb.PutObjectRequest{},
expectedErrContains: "key is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing bucket",
request: &pb.PutObjectRequest{
Key: "mock-object",
},
expectedErrContains: "bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing bucket name",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{},
Key: "mock-object",
},
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing attributes",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
},
Key: "mock-object",
},
expectedErrContains: "attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "missing path",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Key: "mock-object",
},
expectedErrContains: "path is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "file does not exist",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Key: "mock-object",
Path: path.Join(td, "file-does-not-exist"),
},
expectedErrContains: "failed to open file",
expectedErrCode: codes.Internal,
},
{
name: "path is a directory",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Key: "mock-object",
Path: td,
},
expectedErrContains: "path is not a file",
expectedErrCode: codes.InvalidArgument,
},
{
name: "file is empty",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Key: "mock-object",
Path: emptyFilePath,
},
expectedErrContains: "file is empty",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{},
},
},
Key: "mock-object",
Path: validFilePath,
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
Key: "mock-object",
Path: validFilePath,
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
request: validRequest(),
credOpts: []credential.AwsCredentialPersistedStateOption{
func(s *credential.AwsCredentialPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "storage persisted state setup error",
request: validRequest(),
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "putObject error",
request: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectError(errors.New(testPutObjectErr)),
),
),
},
expectedErrContains: "error putting object into s3: test error for PutObject",
expectedErrCode: codes.Internal,
},
{
name: "throttle error",
request: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithPutObjectError(new(throttleErr)),
),
),
},
expectedErrContains: "error putting object into s3: ThrottlingException",
expectedErrCode: codes.Unavailable,
},
{
name: "missing checksum from aws",
request: validRequest(),
credOpts: validSTSMock(),
expectedObject: []byte("CONTENT CHECK"),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
&testMockS3State{
PutObjectBody: []byte("CONTENT CHECK"),
},
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
),
),
},
expectedErrContains: "missing checksum response from aws",
expectedErrCode: codes.Internal,
},
{
name: "mismatched checksum",
request: validRequest(),
credOpts: validSTSMock(),
expectedObject: []byte("CONTENT CHECK"),
storageOpts: func() []awsStoragePersistedStateOption {
hash := sha256.New()
_, err := hash.Write([]byte("CONTENT CHECK"))
require.NoError(t, err)
return []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
&testMockS3State{
PutObjectBody: []byte("CONTENT CHECK"),
},
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{
ChecksumSHA256: aws.String(string(hash.Sum(nil))),
}),
),
),
}
}(),
expectedErrContains: "mismatched checksum",
expectedErrCode: codes.Internal,
},
{
name: "valid file",
request: validRequest(),
credOpts: validSTSMock(),
expectedObject: []byte("CONTENT CHECK"),
storageOpts: func() []awsStoragePersistedStateOption {
hash := sha256.New()
_, err := hash.Write([]byte("CONTENT CHECK"))
require.NoError(t, err)
checksum := base64.StdEncoding.EncodeToString(hash.Sum(nil))
return []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
&testMockS3State{
PutObjectBody: []byte("CONTENT CHECK"),
},
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{
ChecksumSHA256: aws.String(checksum),
}),
),
),
}
}(),
},
{
name: "valid file using dynamic credentials",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Key: "mock-object",
Path: validFilePath,
},
credOpts: validSTSMock(),
expectedObject: []byte("CONTENT CHECK"),
storageOpts: func() []awsStoragePersistedStateOption {
hash := sha256.New()
_, err := hash.Write([]byte("CONTENT CHECK"))
require.NoError(t, err)
checksum := base64.StdEncoding.EncodeToString(hash.Sum(nil))
return []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
&testMockS3State{
PutObjectBody: []byte("CONTENT CHECK"),
},
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{
ChecksumSHA256: aws.String(checksum),
}),
),
),
}
}(),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
if tc.request != nil && len(tc.expectedObject) > 0 {
data, err := os.ReadFile(tc.request.Path)
require.NoError(err)
require.ElementsMatch(data, tc.expectedObject)
}
resp, err := p.PutObject(context.Background(), tc.request)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(tc.expectedErrCode.String(), status.Code(err).String())
return
}
require.NoError(err)
require.NotNil(resp)
hash := sha256.New()
_, err = hash.Write(tc.expectedObject)
require.NoError(err)
require.True(bytes.Equal(hash.Sum(nil), resp.GetChecksumSha_256()))
})
}
}