Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions awsutil/generate_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package awsutil

import (
"context"
"errors"
"fmt"
"net/http"
"os"
Expand All @@ -20,7 +21,17 @@ import (
"github.com/hashicorp/go-hclog"
)

const iamServerIdHeader = "X-Vault-AWS-IAM-Server-ID"
const (
iamServerIdHeader = "X-Vault-AWS-IAM-Server-ID"
defaultStr = "default"
envAwsProfile = "AWS_PROFILE"
)

var (
ErrReadOptsCredChain = errors.New("error reading options in GenerateCredentialChain")
ErrBadStaticCreds = errors.New("static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both)")
ErrLoadConfigWithCredsFailed = errors.New("failed to load SDK's default configurations with given credential options")
)

type CredentialsConfig struct {
// The access key if static credentials are being used
Expand Down Expand Up @@ -164,7 +175,7 @@ func (c *CredentialsConfig) log(level hclog.Level, msg string, args ...interface
}
}

func (c *CredentialsConfig) generateAwsConfigOptions(opts options) []func(*config.LoadOptions) error {
func (c *CredentialsConfig) generateAwsConfigOptions(ctx context.Context, opts options) []func(*config.LoadOptions) error {
var cfgOpts []func(*config.LoadOptions) error

if c.Region != "" {
Expand All @@ -181,16 +192,28 @@ func (c *CredentialsConfig) generateAwsConfigOptions(opts options) []func(*confi

// Add the shared credentials
if opts.withSharedCredentials {
profile := os.Getenv("AWS_PROFILE")
profile := os.Getenv(envAwsProfile)
if profile != "" {
c.Profile = profile
}

// The AWS SDK will check for the 'default' shared profile and include it if it exists. If
// WithSharedConfigProfile is set to 'default' here, and it does not exist the SDK will return an error. So
// only set the config profile if the caller or env has explicitly set one.
// if profile is not set, check for a default profile and add it only if it exists.
if c.Profile != "" {
cfgOpts = append(cfgOpts, config.WithSharedConfigProfile(c.Profile))
} else {
c.Profile = defaultStr
opts := []func(*config.LoadOptions) error{config.WithSharedConfigProfile(defaultStr)}
if c.Filename != "" {
opts = append(opts, config.WithSharedCredentialsFiles([]string{c.Filename}))
}
_, err := config.LoadDefaultConfig(ctx, opts...)
// aws-sdk's special errors don't work with go's errors.Is
_, ok := err.(config.SharedConfigProfileNotExistError)
if !ok {
cfgOpts = append(cfgOpts, config.WithSharedConfigProfile(defaultStr))
}
}

cfgOpts = append(cfgOpts, config.WithSharedCredentialsFiles([]string{c.Filename}))
Expand Down Expand Up @@ -257,17 +280,17 @@ func (c *CredentialsConfig) generateAwsConfigOptions(opts options) []func(*confi
func (c *CredentialsConfig) GenerateCredentialChain(ctx context.Context, opt ...Option) (*aws.Config, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("error reading options in GenerateCredentialChain: %w", err)
return nil, fmt.Errorf("%w: %w", ErrReadOptsCredChain, err)
}

// Have one or the other but not both and not neither
if (c.AccessKey != "" && c.SecretKey == "") || (c.AccessKey == "" && c.SecretKey != "") {
return nil, fmt.Errorf("static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both)")
return nil, ErrBadStaticCreds
}

awsConfig, err := config.LoadDefaultConfig(ctx, c.generateAwsConfigOptions(opts)...)
awsConfig, err := config.LoadDefaultConfig(ctx, c.generateAwsConfigOptions(ctx, opts)...)
if err != nil {
return nil, fmt.Errorf("failed to load SDK's default configurations with given credential options")
return nil, fmt.Errorf("%w: %w", ErrLoadConfigWithCredsFailed, err)
}

if opts.withCredentialsProvider != nil {
Expand Down
131 changes: 113 additions & 18 deletions awsutil/generate_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ package awsutil

import (
"bytes"
"context"
"errors"
"os"
"path"
"slices"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -90,7 +90,6 @@ func TestNewCredentialsConfig(t *testing.T) {
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
Expand Down Expand Up @@ -150,7 +149,6 @@ func TestRetrieveCreds(t *testing.T) {
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
Expand All @@ -159,7 +157,7 @@ func TestRetrieveCreds(t *testing.T) {
require.NoError(err)
require.NotNil(cfg)

awscfg, err := RetrieveCreds(context.Background(), "foo", "bar", "baz", nil, tc.opts...)
awscfg, err := RetrieveCreds(t.Context(), "foo", "bar", "baz", nil, tc.opts...)
Comment thread
emilia-grant marked this conversation as resolved.
if tc.expectedErr != "" {
require.Error(err)
require.EqualError(err, tc.expectedErr)
Expand All @@ -169,7 +167,7 @@ func TestRetrieveCreds(t *testing.T) {
require.NoError(err)
assert.NotNil(awscfg)

creds, err := awscfg.Credentials.Retrieve(context.Background())
creds, err := awscfg.Credentials.Retrieve(t.Context())
require.NoError(err)
assert.Equal("foo", creds.AccessKeyID)
assert.Equal("bar", creds.SecretAccessKey)
Expand All @@ -179,24 +177,35 @@ func TestRetrieveCreds(t *testing.T) {
}

func TestGenerateCredentialChain(t *testing.T) {
// Create a shared creds file with a default profile
dir := t.TempDir()
profileWithDefault := path.Join(dir, "profile_with_default")
f, err := os.Create(profileWithDefault)
require.NoError(t, err)
_, err = f.Write([]byte("[default]\nregion=us-east-2\n"))
require.NoError(t, err)
require.NoError(t, f.Close())
Comment thread
emilia-grant marked this conversation as resolved.

cases := []struct {
name string
opts []Option
expectedErr string
name string
Comment thread
ddebko marked this conversation as resolved.
opts []Option
expectedErr error
ccModFunc func(cc *CredentialsConfig)
additionalAsserts func(t *testing.T, cfg *aws.Config)
}{
{
name: "static cred missing access key",
opts: []Option{
WithSecretKey("foo"),
},
expectedErr: "static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both)",
expectedErr: ErrBadStaticCreds,
},
{
name: "static cred missing secret key",
opts: []Option{
WithAccessKey("foo"),
},
expectedErr: "static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both)",
expectedErr: ErrBadStaticCreds,
},
{
name: "valid static cred",
Expand All @@ -205,9 +214,26 @@ func TestGenerateCredentialChain(t *testing.T) {
WithSecretKey("bar"),
},
},
{
// Note: Region won't match the profile's region because
// NewCredentialsConfig ignores config file's region
name: "creds from shared creds file",
ccModFunc: func(cc *CredentialsConfig) {
cc.Filename = profileWithDefault
},
additionalAsserts: func(t *testing.T, cfg *aws.Config) {
isDefaultProfile := func(cfs any) bool {
configSource, ok := cfs.(config.SharedConfig)
if !ok {
return false
}
return configSource.Profile == defaultStr
}
assert.True(t, slices.ContainsFunc(cfg.ConfigSources, isDefaultProfile))
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
Expand All @@ -216,15 +242,22 @@ func TestGenerateCredentialChain(t *testing.T) {
require.NoError(err)
require.NotNil(cfg)

awscfg, err := cfg.GenerateCredentialChain(context.Background())
if tc.expectedErr != "" {
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
if tc.ccModFunc != nil {
tc.ccModFunc(cfg)
}

awscfg, err := cfg.GenerateCredentialChain(t.Context())
if tc.expectedErr != nil {
assert.ErrorIs(err, tc.expectedErr)
assert.Nil(awscfg)
return
}
require.NoError(err)
assert.NotNil(awscfg)

if tc.additionalAsserts != nil {
tc.additionalAsserts(t, awscfg)
}
})
}
}
Expand All @@ -239,6 +272,28 @@ func TestGenerateAwsConfigOptions(t *testing.T) {
require.NoError(t, err)
require.NoError(t, f.Close())

// Create a shared creds file with a default "profile"
// because this is a creds file, it doesn't use the profile keyword but is
// otherwise treated the same as a config file
profileWithDefault := path.Join(dir, "profile_with_default")
f2, err := os.Create(profileWithDefault)
require.NoError(t, err)
_, err = f2.Write([]byte("[default]\nregion=us-west-1\n"))
require.NoError(t, err)
require.NoError(t, f2.Close())

// Check for default profile in ~/.aws/config because "empty shared profile adds default profile without shared file"
// fails if there is not a default profile present
emptySharedProfileExpectedProfile := ""
home, err := os.UserHomeDir()
require.NoError(t, err)
bs, err := os.ReadFile(path.Join(home, ".aws", "config"))
if err == nil {
if bytes.Contains(bs, []byte("[profile default]")) {
emptySharedProfileExpectedProfile = defaultStr
}
}

cases := []struct {
name string
cfg *CredentialsConfig
Expand Down Expand Up @@ -304,6 +359,47 @@ func TestGenerateAwsConfigOptions(t *testing.T) {
SharedCredentialsFiles: []string{"foobaz"},
},
},
{
// See the setup above for emptySharedProfileExpectedProfile
// This tests that the `SharedConfigProfileNotExistError" check works
// when the default profile lives in the usual ~/.aws/config file
name: "empty shared profile adds default profile without shared file",
cfg: func() *CredentialsConfig {
credCfg, err := NewCredentialsConfig()
require.NoError(t, err)
credCfg.Profile = ""
return credCfg
}(),
opts: options{
withSharedCredentials: true,
},
expectedLoadOptions: config.LoadOptions{
SharedConfigProfile: emptySharedProfileExpectedProfile,
SharedCredentialsFiles: []string{""},
Region: "us-east-1",
},
},
{
// This tests that the `SharedConfigProfileNotExistError" check works
// when the default profile lives in a credentials file
name: "empty shared profile adds default profile with shared file",
cfg: func() *CredentialsConfig {
credCfg, err := NewCredentialsConfig()
require.NoError(t, err)
credCfg.Filename = profileWithDefault
return credCfg
}(),
opts: options{
withSharedCredentials: true,
},
expectedLoadOptions: config.LoadOptions{
SharedConfigProfile: "default",
SharedCredentialsFiles: []string{profileWithDefault},
// Because the profiles aren't actually consumed, the
// region is still the default us-east-1
Region: "us-east-1",
},
},
{
name: "web identity token file credential",
cfg: func() *CredentialsConfig {
Expand Down Expand Up @@ -391,11 +487,10 @@ func TestGenerateAwsConfigOptions(t *testing.T) {
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
opts := tc.cfg.generateAwsConfigOptions(tc.opts)
opts := tc.cfg.generateAwsConfigOptions(t.Context(), tc.opts)
cfgLoadOpts := config.LoadOptions{}
for _, f := range opts {
require.NoError(f(&cfgLoadOpts))
Expand Down Expand Up @@ -430,7 +525,7 @@ func TestGenerateAwsConfigOptions(t *testing.T) {

if tc.expectedStaticCredentials != nil {
require.NotNil(cfgLoadOpts.Credentials)
actualCreds, err := cfgLoadOpts.Credentials.Retrieve(context.Background())
actualCreds, err := cfgLoadOpts.Credentials.Retrieve(t.Context())
require.NoError(err)
assert.Equal(tc.expectedStaticCredentials.AccessKeyID, actualCreds.AccessKeyID)
assert.Equal(tc.expectedStaticCredentials.SecretAccessKey, actualCreds.SecretAccessKey)
Expand Down
3 changes: 1 addition & 2 deletions awsutil/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/hashicorp/go-secure-stdlib/awsutil/v2

go 1.23.3
go 1.24

require (
github.com/aws/aws-sdk-go-v2 v1.32.5
Expand All @@ -27,7 +27,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
Expand Down
Loading
Loading