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:
1 parent
d970294995
commit
2568eb83b8
15 files changed
+858
-210
No files matched your search
@@ -13,7 +13,7 @@ require (
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.33
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0
|
||||
github.com/hashicorp/terraform-json v0.14.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
@@ -21,8 +21,6 @@ require (
|
||||
google.golang.org/protobuf v1.28.1
|
||||
)
|
||||
|
||||
replace github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2 => github.com/hashicorp/go-secure-stdlib/awsutilv2 v0.0.0-20230809054404-9029211590fd
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.33 // indirect
|
||||
|
||||
@@ -94,8 +94,8 @@ github.com/hashicorp/go-kms-wrapping/v2 v2.0.9/go.mod h1:NtMaPhqSlfQ72XWDD2g80o8
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutilv2 v0.0.0-20230809054404-9029211590fd h1:46A3lfywoivyHBOXlXioGDSDo3UJj2aW2ncnSCSJ7bs=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutilv2 v0.0.0-20230809054404-9029211590fd/go.mod h1:XnoCjGsUi+UKk0+0P5TIS2rGKdw7O9ehSJy+kIuffBY=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0 h1:ca5TSI4AgaOncPpyzLDtCGjVEtKukONpeM95vFxXCOQ=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0/go.mod h1:7CUvZtfTp2U0CYQCLzMtS2ngckjAZePSfwrE2aeDP1M=
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
|
||||
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.9 h1:YZ01/aiARPneDUryzjiPGvr3pmjCSC+1WBZKI1P7I7k=
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
@@ -55,10 +55,6 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
|
||||
}
|
||||
delete(unknownFields, ConstAccessKeyId)
|
||||
|
||||
if accessKey != "" && GetCredentialType(accessKey) != Static {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "requires long-term access key beginning with \"AKIA\" prefix"
|
||||
}
|
||||
|
||||
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, required)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
||||
@@ -90,6 +86,9 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
|
||||
// static credential is missing it's access_key_id
|
||||
case accessKey == "" && secretKey != "":
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "missing required value"
|
||||
// dynamic credentials and credential rotation is not supported
|
||||
case len(attrs.RoleArn) > 0 && !attrs.DisableCredentialRotation:
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = "disable_credential_rotation attribute is required when providing a role_arn"
|
||||
// add static credentials
|
||||
case accessKey != "" && secretKey != "":
|
||||
opts = append(opts,
|
||||
|
||||
@@ -6,7 +6,7 @@ package credential
|
||||
import (
|
||||
"testing"
|
||||
|
||||
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"
|
||||
@@ -126,19 +126,6 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with invalid static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("ASIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErrContains: `secrets.access_key_id: requires long-term access key beginning with "AKIA" prefix`,
|
||||
},
|
||||
{
|
||||
name: "with static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
@@ -228,6 +215,7 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
DisableCredentialRotation: true,
|
||||
},
|
||||
expected: &awsutilv2.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
@@ -258,6 +246,20 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
},
|
||||
expectedErrContains: "secrets.access_key_id: conflicts with role_arn value, secrets.role_arn: conflicts with access_key_id and secret_access_key values, secrets.secret_access_key: conflicts with role_arn value",
|
||||
},
|
||||
{
|
||||
name: "with dynamic credentials and no disable credential rotation",
|
||||
secrets: &structpb.Struct{Fields: map[string]*structpb.Value{}},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.disable_credential_rotation: disable_credential_rotation attribute is required when providing a role_arn",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -13,16 +13,30 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type CredentialType int
|
||||
|
||||
const (
|
||||
Unknown CredentialType = iota
|
||||
Static
|
||||
Dynamic
|
||||
// StaticAWS denotes an Access Key Id that begins with "AKIA". These are
|
||||
// long-term access keys, provided by AWS, for an IAM user or an AWS account
|
||||
// root user.
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
||||
StaticAWS CredentialType = iota
|
||||
|
||||
// DynamicAWS denotes the presence of a RoleARN, or an Access Key Id that
|
||||
// begins with "ASIA". The latter are temporary credentials access keys that
|
||||
// are created using AWS STS operations.
|
||||
DynamicAWS
|
||||
|
||||
// StaticOther denotes the presence of an Access Key Id that does not follow
|
||||
// the AKIA/ASIA convention outlined by AWS.
|
||||
StaticOther
|
||||
|
||||
// Unknown is a catch-all for everything else.
|
||||
Unknown
|
||||
)
|
||||
|
||||
// rotationWaitTimeout controls the time we wait for credential rotation to
|
||||
@@ -112,7 +126,7 @@ func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
if GetCredentialType(s.CredentialsConfig.AccessKey) != Static {
|
||||
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
|
||||
return errors.New("invalid credential type")
|
||||
}
|
||||
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutilv2.Option{
|
||||
@@ -134,13 +148,11 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credenti
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
if GetCredentialType(s.CredentialsConfig.AccessKey) != Static {
|
||||
return errors.New("invalid credential type")
|
||||
}
|
||||
|
||||
// Delete old/existing credentials. This action is only possible for static credential types.
|
||||
// This is done with the same credentials to ensure that it has the proper permissions to do it.
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
ct := GetCredentialType(s.CredentialsConfig)
|
||||
if !s.CredsLastRotatedTime.IsZero() && ct == StaticAWS {
|
||||
if err := s.DeleteCreds(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -159,7 +171,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
if GetCredentialType(s.CredentialsConfig.AccessKey) != Static {
|
||||
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
|
||||
return errors.New("invalid credential type")
|
||||
}
|
||||
|
||||
@@ -192,7 +204,8 @@ func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Contex
|
||||
// return a map for long-term credentials with following keys:
|
||||
// access_key_id, secret_access_key & creds_last_rotated_time
|
||||
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
||||
if HasDynamicCredentials(s.CredentialsConfig.AccessKey) {
|
||||
ct := GetCredentialType(s.CredentialsConfig)
|
||||
if ct == DynamicAWS || ct == Unknown {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
@@ -269,29 +282,22 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// GetCredentialType returns the credential type based on the given AWS AccessKey.
|
||||
//
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
||||
// Access key IDs beginning with ASIA are temporary credentials access keys that you create using AWS STS operations.
|
||||
//
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
||||
// Access key IDs beginning with AKIA are long-term access keys for an IAM user or an AWS account root user.
|
||||
func GetCredentialType(accessKey string) CredentialType {
|
||||
if strings.HasPrefix(accessKey, "ASIA") {
|
||||
return Dynamic
|
||||
// GetCredentialType returns the credential type based on the given
|
||||
// AccessKey/RoleARN. See CredentialType definition for more information.
|
||||
func GetCredentialType(cc *awsutilv2.CredentialsConfig) CredentialType {
|
||||
if cc == nil {
|
||||
return Unknown
|
||||
}
|
||||
if strings.HasPrefix(accessKey, "AKIA") {
|
||||
return Static
|
||||
|
||||
if len(cc.RoleARN) > 0 || strings.HasPrefix(cc.AccessKey, "ASIA") {
|
||||
return DynamicAWS
|
||||
}
|
||||
if strings.HasPrefix(cc.AccessKey, "AKIA") {
|
||||
return StaticAWS
|
||||
}
|
||||
if len(cc.AccessKey) > 0 {
|
||||
return StaticOther
|
||||
}
|
||||
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// HasDynamicCredentials returns true if the access key is a dynamic credential type.
|
||||
func HasDynamicCredentials(accessKey string) bool {
|
||||
return strings.HasPrefix(accessKey, "ASIA")
|
||||
}
|
||||
|
||||
// HasStaticCredentials returns true if the access key is a static credential type.
|
||||
func HasStaticCredentials(accessKey string) bool {
|
||||
return strings.HasPrefix(accessKey, "AKIA")
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"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"
|
||||
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/protobuf/types/known/structpb"
|
||||
)
|
||||
@@ -533,30 +533,6 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
|
||||
credentialConfig: &awsutilv2.CredentialsConfig{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "cannot delete dynamic credential type",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutilv2.CredentialsConfig{
|
||||
AccessKey: "ASIAfoobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
CredsLastRotatedTime: time.Now(),
|
||||
},
|
||||
credentialConfig: &awsutilv2.CredentialsConfig{},
|
||||
expectedErr: "invalid credential type",
|
||||
},
|
||||
{
|
||||
name: "cannot delete unknown credential type",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutilv2.CredentialsConfig{
|
||||
AccessKey: "DNEfoobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
CredsLastRotatedTime: time.Now(),
|
||||
},
|
||||
credentialConfig: &awsutilv2.CredentialsConfig{},
|
||||
expectedErr: "invalid credential type",
|
||||
},
|
||||
{
|
||||
name: "deletion error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
@@ -815,74 +791,53 @@ func TestAwsCatalogPersistedState_GenerateCredentialChain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasStaticCredentials(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
accessKey string
|
||||
expected bool
|
||||
func TestGetCredentialType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
credConfig *awsutilv2.CredentialsConfig
|
||||
expectedCredType CredentialType
|
||||
}{
|
||||
{
|
||||
name: "dynamic credential",
|
||||
accessKey: "ASIAEXAMPLE",
|
||||
expected: false,
|
||||
name: "nil credential config",
|
||||
credConfig: nil,
|
||||
expectedCredType: Unknown,
|
||||
},
|
||||
{
|
||||
name: "static credential",
|
||||
accessKey: "AKIAEXAMPLE",
|
||||
expected: true,
|
||||
name: "empty credential config",
|
||||
credConfig: &awsutilv2.CredentialsConfig{},
|
||||
expectedCredType: Unknown,
|
||||
},
|
||||
{
|
||||
name: "other credential",
|
||||
accessKey: "EXAMPLE",
|
||||
expected: false,
|
||||
name: "static aws",
|
||||
credConfig: &awsutilv2.CredentialsConfig{AccessKey: "AKIAfoobar"},
|
||||
expectedCredType: StaticAWS,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
accessKey: "",
|
||||
expected: false,
|
||||
name: "static other",
|
||||
credConfig: &awsutilv2.CredentialsConfig{AccessKey: "cK3kNFa24foobar"},
|
||||
expectedCredType: StaticOther,
|
||||
},
|
||||
{
|
||||
name: "dynamic aws - role arn",
|
||||
credConfig: &awsutilv2.CredentialsConfig{RoleARN: "arn:aws:iam::123456789012:role/S3Access"},
|
||||
expectedCredType: DynamicAWS,
|
||||
},
|
||||
{
|
||||
name: "dynamic aws - access key",
|
||||
credConfig: &awsutilv2.CredentialsConfig{AccessKey: "ASIAfoobar"},
|
||||
expectedCredType: DynamicAWS,
|
||||
},
|
||||
{
|
||||
name: "dynamic aws - role arn and access key",
|
||||
credConfig: &awsutilv2.CredentialsConfig{AccessKey: "ASIAfoobar", RoleARN: "arn:aws:iam::123456789012:role/S3Access"},
|
||||
expectedCredType: DynamicAWS,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(tc.expected, HasStaticCredentials(tc.accessKey))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDynamicCredentials(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
accessKey string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "dynamic credential",
|
||||
accessKey: "ASIAEXAMPLE",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "static credential",
|
||||
accessKey: "AKIAEXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "other credential",
|
||||
accessKey: "EXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
accessKey: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(tc.expected, HasDynamicCredentials(tc.accessKey))
|
||||
ct := GetCredentialType(tc.credConfig)
|
||||
require.Equal(t, tc.expectedCredType, ct)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/aws/smithy-go"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
awsutilv2 "github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
tfjson "github.com/hashicorp/terraform-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
Reference in new issue
Block a user