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.
This commit is contained in:
Hugo committed 2023-09-21 18:45:35 +01:00
1 parent d970294995
commit 2568eb83b8
15 files changed
+858 -210

No files matched your search

+3 -4
View File
@@ -83,8 +83,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
}
// Try to rotate static credentials
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if !catalogAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
@@ -160,7 +159,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
@@ -244,7 +243,7 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
}
// try to delete static credentials
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if !credState.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
)
+15 -29
View File
@@ -76,17 +76,10 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
}
// Try to rotate static credentials
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if !storageAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
} else {
// Simply validate if we aren't rotating.
if err := credState.ValidateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
}
// Try to rotate AWS static credentials
if cred.GetCredentialType(credConfig) == cred.StaticAWS && !storageAttributes.DisableCredentialRotation {
if err := credState.RotateCreds(ctx); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
}
@@ -156,15 +149,18 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
// Verify the incoming credentials are valid and return any errors to the
// user if they're not. Note this doesn't validate the credentials against
// AWS - it only does logical validation on the fields.
updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
// We will be updating credentials when one of two changes occur:
// 1. the new bucket has secrets for static credentials, in which case we need to delete the old static credentials
// 2. the new bucket has a different roleARN value for dynamic credentials
if newBucket.GetSecrets() != nil || newStorageAttributes.RoleArn != oldStorageAttributes.RoleArn {
updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
// Replace the existing credential state.
// This checks the timestamp on the last rotation time as well
// and deletes the credentials if we are managing them
@@ -174,9 +170,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
}
}
credentialType := cred.GetCredentialType(credState.CredentialsConfig.AccessKey)
switch credentialType {
case cred.Static:
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
// This is a validate check to make sure that we aren't disabling
// rotation for credentials currently being managed by rotation.
// This is not allowed.
@@ -193,16 +187,8 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
}
}
case cred.Dynamic:
// We cannot allow credential rotation for dynamic credentials
if !newStorageAttributes.DisableCredentialRotation {
return nil, status.Errorf(codes.InvalidArgument, "cannot enable credential rotation for dynamic credential type")
}
case cred.Unknown:
fallthrough
default:
return nil, status.Errorf(codes.InvalidArgument, "unknown credential type")
}
storageState, err := newAwsStoragePersistedState(
append([]awsStoragePersistedStateOption{
withCredentials(credState),
@@ -268,7 +254,7 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
}
// try to delete static credentials
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
if !credState.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
+736 -33
View File
@@ -20,10 +20,12 @@ import (
"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"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -104,6 +106,23 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
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(),
@@ -141,21 +160,6 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "validation error",
req: validRequest(),
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutilv2.Option{
awsutilv2.WithSTSAPIFunc(
awsutilv2.NewMockSTS(
awsutilv2.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
),
),
}),
},
expectedErrContains: fmt.Sprintf("error during credential validation: error validating credentials: %s", testGetCallerIdentityErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "dryRunValidation failed putObject",
req: validRequest(),
@@ -324,6 +328,44 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
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(
[]awsutilv2.Option{
awsutilv2.WithSTSAPIFunc(
awsutilv2.NewMockSTS(
awsutilv2.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 {
@@ -344,8 +386,10 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
require.NotNil(resp)
require.NotNil(resp.GetPersisted())
require.NotNil(resp.GetPersisted().GetData())
actualPersistedData := resp.GetPersisted().GetData().AsMap()
if len(actualPersistedData) != 0 {
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])
@@ -383,12 +427,14 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
}
}
cases := []struct {
name string
req *pb.OnUpdateStorageBucketRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
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",
@@ -552,6 +598,36 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
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{
@@ -796,6 +872,11 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
),
),
},
expectedPersistedData: map[string]any{
credential.ConstAccessKeyId: "AKIA_one",
credential.ConstSecretAccessKey: "two",
},
expectCredsLastRotatedTime: true,
},
{
name: "success",
@@ -811,6 +892,488 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
),
),
},
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.Internal,
},
{
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.Internal,
},
{
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)),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.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([]awsutilv2.Option{
awsutilv2.WithIAMAPIFunc(
awsutilv2.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.Internal,
},
{
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{}},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutilv2.Option{
awsutilv2.WithSTSAPIFunc(
awsutilv2.NewMockSTS(),
),
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutilv2.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([]awsutilv2.Option{
awsutilv2.WithSTSAPIFunc(
awsutilv2.NewMockSTS(),
),
awsutilv2.WithIAMAPIFunc(
awsutilv2.NewMockIAM(
awsutilv2.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutilv2.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,
},
}
@@ -824,6 +1387,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
}
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
@@ -833,18 +1397,18 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
require.NotNil(resp.GetPersisted())
require.NotNil(resp.GetPersisted().GetData())
var disableCredentialRotation bool
expectedAccessKey := tc.req.Persisted.Data.AsMap()[credential.ConstAccessKeyId]
expectedSecretKey := tc.req.Persisted.Data.AsMap()[credential.ConstSecretAccessKey]
if value, ok := tc.req.GetNewBucket().GetAttributes().AsMap()[credential.ConstDisableCredentialRotation]; ok {
disableCredentialRotation = value.(bool)
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 !disableCredentialRotation {
expectedAccessKey = "AKIA_one"
expectedSecretKey = "two"
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])
}
}
require.Equal(expectedAccessKey, resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId])
require.Equal(expectedSecretKey, resp.GetPersisted().GetData().AsMap()[credential.ConstSecretAccessKey])
})
}
}
@@ -988,6 +1552,23 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
}),
},
},
{
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 {
@@ -1171,6 +1752,37 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
),
},
},
{
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 {
@@ -1401,6 +2013,32 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
),
},
},
{
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 {
@@ -1602,6 +2240,34 @@ func TestStoragePlugin_GetObject(t *testing.T) {
),
},
},
{
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 {
@@ -1970,6 +2636,43 @@ func TestStoragePlugin_PutObject(t *testing.T) {
}
}(),
},
{
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 {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing"
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
)
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"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"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/structpb"