diff --git a/internal/credential/attributes.go b/internal/credential/attributes.go index eac891a..b94af11 100644 --- a/internal/credential/attributes.go +++ b/internal/credential/attributes.go @@ -37,7 +37,7 @@ type CredentialAttributes struct { // CredentialsConfig used for configuring an AWS session. An status error is returned // with an InvalidArgument code if any unrecognized fields are found in the protobuf // struct input. -func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes) (*awsutil.CredentialsConfig, error) { +func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool) (*awsutil.CredentialsConfig, error) { // initialize secrets if it is nil // secrets can be nil because static credentials are optional if secrets == nil { @@ -117,6 +117,17 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes) opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags)) } + if dualStack { + opts = append(opts, []awsutil.Option{ + awsutil.WithIamEndpointResolver(&iamEndpointResolver{ + dualStack: dualStack, + }), + awsutil.WithStsEndpointResolver(&stsEndpointResolver{ + dualStack: dualStack, + }), + }...) + } + cfg, err := awsutil.NewCredentialsConfig(opts...) if err != nil { return nil, errors.BadRequestStatus(err.Error()) diff --git a/internal/credential/attributes_test.go b/internal/credential/attributes_test.go index b21a662..3619f0b 100644 --- a/internal/credential/attributes_test.go +++ b/internal/credential/attributes_test.go @@ -106,6 +106,7 @@ func TestGetCredentialsConfig(t *testing.T) { name string secrets *structpb.Struct attrs *CredentialAttributes + dualStack bool expected *awsutil.CredentialsConfig expectedErrContains string }{ @@ -135,6 +136,24 @@ func TestGetCredentialsConfig(t *testing.T) { Region: "us-west-2", }, }, + { + name: "with dualstack", + secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), + ConstSecretAccessKey: structpb.NewStringValue("bazqux"), + }, + }, + dualStack: true, + attrs: &CredentialAttributes{ + Region: "us-west-2", + }, + expected: &awsutil.CredentialsConfig{ + AccessKey: "AKIAfoobar", + SecretKey: "bazqux", + Region: "us-west-2", + }, + }, { name: "unknown fields", secrets: &structpb.Struct{ @@ -233,7 +252,7 @@ func TestGetCredentialsConfig(t *testing.T) { t.Run(tc.name, func(t *testing.T) { require := require.New(t) - actual, err := GetCredentialsConfig(tc.secrets, tc.attrs) + actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.dualStack) if tc.expectedErrContains != "" { require.Error(err) require.Contains(err.Error(), tc.expectedErrContains) @@ -249,6 +268,13 @@ func TestGetCredentialsConfig(t *testing.T) { require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId) require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName) require.Equal(tc.expected.RoleTags, actual.RoleTags) + if tc.dualStack { + require.NotNil(actual.IAMEndpointResolver) + require.NotNil(actual.STSEndpointResolver) + } else { + require.Nil(actual.IAMEndpointResolver) + require.Nil(actual.STSEndpointResolver) + } }) } } diff --git a/internal/credential/state.go b/internal/credential/state.go index 15555f5..72b95f6 100644 --- a/internal/credential/state.go +++ b/internal/credential/state.go @@ -11,7 +11,10 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + smithyEndpoints "github.com/aws/smithy-go/endpoints" "github.com/hashicorp/boundary-plugin-aws/internal/errors" "github.com/hashicorp/boundary-plugin-aws/internal/values" "github.com/hashicorp/go-secure-stdlib/awsutil/v2" @@ -237,7 +240,7 @@ func (s *AwsCredentialPersistedState) ToMap() map[string]any { // AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input // and returns a AwsCredentialPersistedState used for configuring an AWS session. -func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) { +func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) { // initialize secrets if it is nil // secrets can be nil because static credentials are optional if secrets == nil { @@ -292,6 +295,17 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede if len(attrs.RoleTags) != 0 { awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags)) } + if dualStack { + awsOpts = append(awsOpts, []awsutil.Option{ + awsutil.WithIamEndpointResolver(&iamEndpointResolver{ + dualStack: dualStack, + }), + awsutil.WithStsEndpointResolver(&stsEndpointResolver{ + dualStack: dualStack, + }), + }...) + } + credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...) if err != nil { return nil, err @@ -321,3 +335,23 @@ func GetCredentialType(cc *awsutil.CredentialsConfig) CredentialType { return Unknown } + +type iamEndpointResolver struct { + dualStack bool +} + +func (e *iamEndpointResolver) ResolveEndpoint(ctx context.Context, params iam.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) { + params.UseDualStack = aws.Bool(e.dualStack) + resolver, err = iam.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) + return +} + +type stsEndpointResolver struct { + dualStack bool +} + +func (e *stsEndpointResolver) ResolveEndpoint(ctx context.Context, params sts.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) { + params.UseDualStack = aws.Bool(e.dualStack) + resolver, err = sts.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) + return +} diff --git a/internal/credential/state_test.go b/internal/credential/state_test.go index 2bc8b7d..1cda1a0 100644 --- a/internal/credential/state_test.go +++ b/internal/credential/state_test.go @@ -112,6 +112,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) { name string secrets *structpb.Struct attrs *CredentialAttributes + dualStack bool opts []AwsCredentialPersistedStateOption expected *AwsCredentialPersistedState expectedErr string @@ -174,6 +175,26 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) { }, }, }, + { + name: "with dualstack static credentials", + secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("foobar"), + ConstSecretAccessKey: structpb.NewStringValue("bazqux"), + }, + }, + dualStack: true, + attrs: &CredentialAttributes{ + Region: "us-west-2", + }, + expected: &AwsCredentialPersistedState{ + CredentialsConfig: &awsutil.CredentialsConfig{ + Region: "us-west-2", + AccessKey: "foobar", + SecretKey: "bazqux", + }, + }, + }, { name: "bad last rotated time", secrets: &structpb.Struct{ @@ -318,7 +339,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) { t.Run(tc.name, func(t *testing.T) { require := require.New(t) - actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.opts...) + actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.dualStack, tc.opts...) if tc.expectedErr != "" { require.EqualError(err, tc.expectedErr) return @@ -336,6 +357,13 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) { require.Equal(tc.expected.CredentialsConfig.RoleExternalId, actual.CredentialsConfig.RoleExternalId) require.Equal(tc.expected.CredentialsConfig.RoleSessionName, actual.CredentialsConfig.RoleSessionName) require.Equal(tc.expected.CredentialsConfig.RoleTags, actual.CredentialsConfig.RoleTags) + if tc.dualStack { + require.NotNil(actual.CredentialsConfig.STSEndpointResolver) + require.NotNil(actual.CredentialsConfig.IAMEndpointResolver) + } else { + require.Nil(actual.CredentialsConfig.STSEndpointResolver) + require.Nil(actual.CredentialsConfig.IAMEndpointResolver) + } } }) } diff --git a/plugin/service/host/plugin.go b/plugin/service/host/plugin.go index 1b9f4a0..f87e6f9 100644 --- a/plugin/service/host/plugin.go +++ b/plugin/service/host/plugin.go @@ -51,7 +51,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo if err != nil { return nil, err } - credConfig, err := credential.GetCredentialsConfig(catalog.GetSecrets(), catalogAttributes.CredentialAttributes) + credConfig, err := credential.GetCredentialsConfig(catalog.GetSecrets(), catalogAttributes.CredentialAttributes, catalogAttributes.DualStack) if err != nil { return nil, err } @@ -135,6 +135,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo credState, err := credential.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetSecrets(), oldCatalogAttributes.CredentialAttributes, + oldCatalogAttributes.DualStack, p.testCredStateOpts...) if err != nil { return nil, errors.BadRequestStatus("error loading persisted state: %s", err) @@ -143,7 +144,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo // 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 := credential.GetCredentialsConfig(newCatalog.GetSecrets(), newCatalogAttributes.CredentialAttributes) + updatedCredentials, err := credential.GetCredentialsConfig(newCatalog.GetSecrets(), newCatalogAttributes.CredentialAttributes, newCatalogAttributes.DualStack) if err != nil { return nil, err } @@ -260,6 +261,7 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo credState, err := credential.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetSecrets(), catalogAttributes.CredentialAttributes, + catalogAttributes.DualStack, p.testCredStateOpts...) if err != nil { return nil, errors.BadRequestStatus("error loading persisted state: %s", err) @@ -336,6 +338,7 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest credState, err := credential.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetSecrets(), catalogAttributes.CredentialAttributes, + catalogAttributes.DualStack, p.testCredStateOpts...) if err != nil { return nil, errors.BadRequestStatus("error loading persisted state: %s", err) @@ -398,6 +401,7 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest credState, err := credential.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetSecrets(), catalogAttributes.CredentialAttributes, + catalogAttributes.DualStack, p.testCredStateOpts...) if err != nil { return nil, errors.BadRequestStatus("error loading persisted state: %s", err) @@ -470,6 +474,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (* credState, err := credential.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetSecrets(), catalogAttributes.CredentialAttributes, + catalogAttributes.DualStack, p.testCredStateOpts...) if err != nil { return nil, errors.BadRequestStatus("error loading persisted state: %s", err) diff --git a/plugin/service/storage/plugin.go b/plugin/service/storage/plugin.go index b35e387..212e9d5 100644 --- a/plugin/service/storage/plugin.go +++ b/plugin/service/storage/plugin.go @@ -64,7 +64,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre return nil, err } - credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes) + credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack) if err != nil { return nil, err } @@ -147,6 +147,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd credState, err := cred.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetData(), oldStorageAttributes.CredentialAttributes, + oldStorageAttributes.DualStack, p.testCredStateOpts...) if err != nil { return nil, errors.BadRequestStatus("error loading persisted state: %s", err) @@ -155,7 +156,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd // 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) + updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, newStorageAttributes.DualStack) if err != nil { return nil, err } @@ -264,6 +265,7 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel credState, err := cred.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetData(), storageAttributes.CredentialAttributes, + storageAttributes.DualStack, p.testCredStateOpts...) if err != nil { return nil, errors.BadRequestStatus("error loading persisted state: %s", err) @@ -318,7 +320,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques return nil, err } - credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes) + credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack) if err != nil { return nil, err } @@ -389,7 +391,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat return nil, err } - credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes) + credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack) if err != nil { return nil, err } @@ -447,7 +449,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu return err } - credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes) + credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack) if err != nil { return err } @@ -561,7 +563,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) return nil, err } - credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes) + credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack) if err != nil { return nil, err } @@ -659,7 +661,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects return nil, err } - credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes) + credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack) if err != nil { return nil, err }