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

+1 -3
View File
@@ -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
+2 -2
View File
@@ -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=
+4 -5
View File
@@ -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,
+16 -14
View File
@@ -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 {
+39 -33
View File
@@ -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")
}
+36 -81
View File
@@ -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)
})
}
}
+1 -1
View File
@@ -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"
)
+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"
+1 -1
View File
@@ -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"