From 0836e402deb23c5033a352f5638744f3a450c0ae Mon Sep 17 00:00:00 2001 From: Micah Hausler Date: Sat, 31 Mar 2018 23:07:20 -0700 Subject: [PATCH 1/5] aws/credential: Add credential_process provider --- .gitignore | 1 + aws/credentials/example.ini | 4 + aws/credentials/process_provider.go | 228 +++++++++++++++++++++++ aws/credentials/process_provider_test.go | 143 ++++++++++++++ aws/defaults/defaults.go | 7 +- aws/session/session.go | 5 + aws/session/shared_config.go | 10 + 7 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 aws/credentials/process_provider.go create mode 100644 aws/credentials/process_provider_test.go diff --git a/.gitignore b/.gitignore index fb11ceca011..86fb2b74362 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ awstesting/integration/smoke/_test/ /vendor/pkg/ /vendor/src/ /private/model/cli/gen-api/gen-api +.*.swp diff --git a/aws/credentials/example.ini b/aws/credentials/example.ini index 7fc91d9d204..84a3536a4f0 100644 --- a/aws/credentials/example.ini +++ b/aws/credentials/example.ini @@ -2,6 +2,7 @@ aws_access_key_id = accessKey aws_secret_access_key = secret aws_session_token = token +credential_process = /bin/echo {"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": "2000-01-01T00:00:00-00:00"} [no_token] aws_access_key_id = accessKey @@ -10,3 +11,6 @@ aws_secret_access_key = secret [with_colon] aws_access_key_id: accessKey aws_secret_access_key: secret + +[profile process] +credential_process = /bin/echo {"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenProcess", "Expiration": "2000-01-01T00:00:00-00:00"} diff --git a/aws/credentials/process_provider.go b/aws/credentials/process_provider.go new file mode 100644 index 00000000000..2541cfe0eb9 --- /dev/null +++ b/aws/credentials/process_provider.go @@ -0,0 +1,228 @@ +package credentials + +import ( + "encoding/json" + "os" + "os/exec" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/internal/shareddefaults" + "github.com/go-ini/ini" +) + +// ProcessProviderName provides a name of Process provider +const ProcessProviderName = "ProcessProvider" + +var ( + // ErrProcessProviderHomeNotFound is emitted when the user directory cannot be found. + ErrProcessProviderHomeNotFound = awserr.New("UserHomeNotFound", "user home directory not found.", nil) +) + +// A ProcessProvider retrieves credentials from a process specified as +// 'credential_process' in a profile, and keeps track if those credentials are +// expired. +// +// Profile ini file example: $HOME/.aws/config +type ProcessProvider struct { + Expiry + + // Path to the shared config file. + // + // If empty will look for "AWS_CONFIG_FILE" env variable. If the + // env value is empty will default to current user's home directory. + // Linux/OSX: "$HOME/.aws/config" + // Windows: "%USERPROFILE%\.aws\config" + Filename string + + // AWS Profile to retrieve the 'credential_process' from. If empty + // will default to environment variable "AWS_PROFILE" or "default" if + // environment variable is also not set. + Profile string + + // ExpiryWindow will allow the credentials to trigger refreshing prior to + // the credentials actually expiring. This is beneficial so race conditions + // with expiring credentials do not cause request to fail unexpectedly + // due to ExpiredTokenException exceptions. + // + // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // 10 seconds before the credentials are actually expired. + // + // If ExpiryWindow is 0 or less it will be ignored. + ExpiryWindow time.Duration + + // A function to perform the credential_process execution. Can be replaced + // for testing + executionFunc func(process string) ([]byte, error) +} + +// NewProcessProvider returns a pointer to a new Credentials object +// wrapping the Profile file provider. +func NewProcessProvider(filename, profile string) *Credentials { + return NewCredentials(&ProcessProvider{ + Filename: filename, + Profile: profile, + executionFunc: executeCredentialProcess, + }) +} + +// Retrieve executes the 'credential_process' and returns the credentials +func (p *ProcessProvider) Retrieve() (Value, error) { + filename, err := p.filename() + if err != nil { + return Value{ProviderName: ProcessProviderName}, err + } + + creds, err := p.loadProfile(filename, p.profile()) + if err != nil { + return Value{ProviderName: ProcessProviderName}, err + } + + return creds, nil +} + +// loadProfiles executes the 'credential_process' from the profile of the given +// file, and returns the credentials retrieved. An error will be returned if it +// fails to read from the file, or the data is invalid. +func (p *ProcessProvider) loadProfile(filename, profile string) (Value, error) { + config, err := ini.Load(filename) + if err != nil { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "failed to load shared config file", err) + } + + var iniProfile *ini.Section + if len(profile) == 0 || profile == "default" { + iniProfile, err = config.GetSection("default") + } else { + iniProfile, err = config.GetSection("profile " + profile) + } + if err != nil { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "failed to get profile", err) + } + + process, err := iniProfile.GetKey("credential_process") + if err != nil { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "No credential_process specified", err) + } + return p.credentialProcess(process.String()) +} + +type credentialProcessResponse struct { + Version int + AccessKeyID string `json:"AccessKeyId"` + SecretAccessKey string + SessionToken string + Expiration string +} + +func executeCredentialProcess(process string) ([]byte, error) { + processArgs := strings.Split(process, " ") + var command string + var cmdArgs []string + + if len(processArgs) == 1 { + command = processArgs[0] + } else if len(processArgs) > 1 { + command = processArgs[0] + cmdArgs = processArgs[1:] + } + + // TODO: Check for executability? + _, err := os.Stat(command) + if os.IsNotExist(err) { + return nil, awserr.New("ProcessProviderLoad", "credential_process "+command+" not found", err) + } else if err != nil { + return nil, awserr.New("ProcessProviderLoad", "failed to open credential_process "+command, err) + } + + // Execute command + cmd := exec.Command(command, cmdArgs...) + cmd.Env = os.Environ() + out, err := cmd.Output() + if err != nil { + return nil, awserr.New("ProcessProviderLoad", "Error executing credential_process", err) + } + return out, nil +} + +func (p *ProcessProvider) credentialProcess(process string) (Value, error) { + out, err := p.executionFunc(process) + if err != nil { + return Value{ProviderName: ProcessProviderName}, err + } + + // Serialize and validate response + resp := &credentialProcessResponse{} + err = json.Unmarshal(out, resp) + if err != nil { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "Error parsing credential_process output: "+string(out), err) + } + if resp.Version != 1 { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", " Version in credential_process output is not 1", err) + } + if len(resp.AccessKeyID) == 0 { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", " Missing required AccessKeyId in credential_process output", err) + } + if len(resp.SecretAccessKey) == 0 { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", " Missing required SecretAccessKey in credential_process output", err) + } + + // Handle expiration + if len(resp.Expiration) > 0 { + expiry, err := time.Parse(time.RFC3339, resp.Expiration) + if err != nil { + return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "Error parsing expiration of credential_process output: "+resp.Expiration, err) + } + p.SetExpiration(expiry, p.ExpiryWindow) + } + + return Value{ + ProviderName: ProcessProviderName, + AccessKeyID: resp.AccessKeyID, + SecretAccessKey: resp.SecretAccessKey, + SessionToken: resp.SessionToken, + }, nil + +} + +// filename returns the filename to use to read AWS shared credentials. +// +// Will return an error if the user's home directory path cannot be found. +func (p *ProcessProvider) filename() (string, error) { + if len(p.Filename) != 0 { + return p.Filename, nil + } + + if p.Filename = os.Getenv("AWS_CONFIG_FILE"); len(p.Filename) != 0 { + return p.Filename, nil + } + + if home := shareddefaults.UserHomeDir(); len(home) == 0 { + // Backwards compatibility of home directly not found error being returned. + // This error is too verbose, failure when opening the file would of been + // a better error to return. + return "", ErrProcessProviderHomeNotFound + } + + p.Filename = shareddefaults.SharedConfigFilename() + + return p.Filename, nil +} + +// profile returns the AWS shared config profile. If empty will read +// environment variable "AWS_PROFILE". If that is not set profile will +// return "default". +func (p *ProcessProvider) profile() string { + if p.Profile == "" { + p.Profile = os.Getenv("AWS_PROFILE") + } + if p.Profile == "" { + p.Profile = os.Getenv("AWS_DEFAULT_PROFILE") + } + if p.Profile == "" { + p.Profile = "default" + } + + return p.Profile +} diff --git a/aws/credentials/process_provider_test.go b/aws/credentials/process_provider_test.go new file mode 100644 index 00000000000..a273cb59b41 --- /dev/null +++ b/aws/credentials/process_provider_test.go @@ -0,0 +1,143 @@ +package credentials + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProcessProvider(t *testing.T) { + os.Clearenv() + + p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: executeCredentialProcess} + creds, err := p.Retrieve() + assert.Nil(t, err, "Expect no error") + + assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") + assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") + assert.Equal(t, "tokenProcess", creds.SessionToken, "Expect session token to match") +} + +func fakeExectuteCredsExpired(process string) ([]byte, error) { + return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": "2000-01-01T00:00:00-00:00"}`), nil +} + +func TestProcessProviderIsExpired(t *testing.T) { + os.Clearenv() + + p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsExpired} + + assert.True(t, p.IsExpired(), "Expect creds to be expired before retrieve") +} + +func TestProcessProviderWithAWS_CONFIG_FILE(t *testing.T) { + os.Clearenv() + os.Setenv("AWS_CONFIG_FILE", "example.ini") + os.Setenv("AWS_DEFAULT_PROFILE", "process") + p := ProcessProvider{Filename: "", Profile: "", executionFunc: executeCredentialProcess} + creds, err := p.Retrieve() + + assert.Nil(t, err, "Expect no error") + + assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") + assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") + assert.Equal(t, "tokenProcess", creds.SessionToken, "Expect session token to match") +} + +func TestProcessProviderWithAWS_CONFIG_FILEAbsPath(t *testing.T) { + os.Clearenv() + wd, err := os.Getwd() + assert.NoError(t, err) + os.Setenv("AWS_CONFIG_FILE", filepath.Join(wd, "example.ini")) + p := ProcessProvider{executionFunc: executeCredentialProcess} + creds, err := p.Retrieve() + assert.Nil(t, err, "Expect no error") + + assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") + assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") + assert.Equal(t, "tokenDefault", creds.SessionToken, "Expect session token to match") +} + +func fakeExectuteCredsSuccess(process string) ([]byte, error) { + return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenFake", "Expiration": "2000-01-01T00:00:00-00:00"}`), nil +} + +func TestProcessProviderWithAWS_PROFILE(t *testing.T) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "process") + + p := ProcessProvider{Filename: "example.ini", Profile: "", executionFunc: fakeExectuteCredsSuccess} + creds, err := p.Retrieve() + assert.Nil(t, err, "Expect no error") + + assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") + assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") + assert.Equal(t, "tokenFake", creds.SessionToken, "Expect token to match") +} + +func fakeExectuteCredsFailMalformed(process string) ([]byte, error) { + return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": `), nil +} + +func TestProcessProviderMalformed(t *testing.T) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "process") + + p := ProcessProvider{Filename: "example.ini", Profile: "", executionFunc: fakeExectuteCredsFailMalformed} + _, err := p.Retrieve() + assert.NotNil(t, err, "Expect an error") +} + +func fakeExectuteCredsNoToken(process string) ([]byte, error) { + return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret"}`), nil +} + +func TestProcessProviderNoToken(t *testing.T) { + os.Clearenv() + + p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsNoToken} + creds, err := p.Retrieve() + assert.Nil(t, err, "Expect no error") + assert.Empty(t, creds.SessionToken, "Expect no token") +} + +func fakeExectuteCredsFailVersion(process string) ([]byte, error) { + return []byte(`{"Version": 2, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault"}`), nil +} + +func TestProcessProviderWrongVersion(t *testing.T) { + os.Clearenv() + p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsFailVersion} + _, err := p.Retrieve() + assert.NotNil(t, err, "Expect an error") +} + +func fakeExectuteCredsFailExpiration(process string) ([]byte, error) { + return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": "20222"}`), nil +} +func TestProcessProviderBadExpiry(t *testing.T) { + os.Clearenv() + p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsFailExpiration} + _, err := p.Retrieve() + assert.NotNil(t, err, "Expect an error") +} + +func BenchmarkProcessProvider(b *testing.B) { + os.Clearenv() + + p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: executeCredentialProcess} + _, err := p.Retrieve() + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := p.Retrieve() + if err != nil { + b.Fatal(err) + } + } +} diff --git a/aws/defaults/defaults.go b/aws/defaults/defaults.go index 23bb639e018..2dddf139b78 100644 --- a/aws/defaults/defaults.go +++ b/aws/defaults/defaults.go @@ -93,7 +93,12 @@ func Handlers() request.Handlers { func CredChain(cfg *aws.Config, handlers request.Handlers) *credentials.Credentials { return credentials.NewCredentials(&credentials.ChainProvider{ VerboseErrors: aws.BoolValue(cfg.CredentialsChainVerboseErrors), - Providers: CredProviders(cfg, handlers), + Providers: []credentials.Provider{ + &credentials.EnvProvider{}, + &credentials.SharedCredentialsProvider{Filename: "", Profile: ""}, + &credentials.ProcessProvider{Filename: "", Profile: ""}, + RemoteCredProvider(*cfg, handlers), + }, }) } diff --git a/aws/session/session.go b/aws/session/session.go index e7c156e8b12..2d929be54a9 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -534,6 +534,11 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg share cfg.Credentials = credentials.NewStaticCredentialsFromCreds( sharedCfg.Creds, ) + } else if envCfg.EnableSharedConfig && len(sharedCfg.CredentialProcess) > 0 { + cfg.Credentials = credentials.NewProcessProvider( + envCfg.SharedConfigFile, + envCfg.Profile, + ) } else { // Fallback to default credentials provider, include mock errors // for the credential chain so user can identify why credentials diff --git a/aws/session/shared_config.go b/aws/session/shared_config.go index 427b8a4e997..bf9fe3f9748 100644 --- a/aws/session/shared_config.go +++ b/aws/session/shared_config.go @@ -28,6 +28,8 @@ const ( // endpoint discovery group enableEndpointDiscoveryKey = `endpoint_discovery_enabled` // optional + // External Credential Process + credentialProcessKey = `credential_process` // DefaultSharedConfigProfile is the default profile to be used when // loading configuration from the config files if another profile name @@ -60,6 +62,9 @@ type sharedConfig struct { AssumeRole assumeRoleConfig AssumeRoleSource *sharedConfig + // An external process to request credentials + CredentialProcess string + // Region is the region the SDK should use for looking up AWS service endpoints // and signing requests. // @@ -223,6 +228,11 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile) e } } + credentialProcess := section.Key(credentialProcessKey).String() + if len(credentialProcess) > 0 { + cfg.CredentialProcess = credentialProcess + } + // Region if v := section.String(regionKey); len(v) > 0 { cfg.Region = v From 79aad5e694cac6fc76663ce44b772a00373ca79f Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 16 Oct 2018 18:37:03 -0400 Subject: [PATCH 2/5] aws/credentials: Rework credential_process provider --- .gitignore | 1 - aws/credentials/process_provider.go | 228 ---------- aws/credentials/processcreds/provider.go | 421 ++++++++++++++++++ .../provider_test.go} | 2 +- aws/defaults/defaults.go | 7 +- aws/session/session.go | 8 +- aws/session/shared_config.go | 6 +- 7 files changed, 430 insertions(+), 243 deletions(-) delete mode 100644 aws/credentials/process_provider.go create mode 100644 aws/credentials/processcreds/provider.go rename aws/credentials/{process_provider_test.go => processcreds/provider_test.go} (99%) diff --git a/.gitignore b/.gitignore index 86fb2b74362..fb11ceca011 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ awstesting/integration/smoke/_test/ /vendor/pkg/ /vendor/src/ /private/model/cli/gen-api/gen-api -.*.swp diff --git a/aws/credentials/process_provider.go b/aws/credentials/process_provider.go deleted file mode 100644 index 2541cfe0eb9..00000000000 --- a/aws/credentials/process_provider.go +++ /dev/null @@ -1,228 +0,0 @@ -package credentials - -import ( - "encoding/json" - "os" - "os/exec" - "strings" - "time" - - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/internal/shareddefaults" - "github.com/go-ini/ini" -) - -// ProcessProviderName provides a name of Process provider -const ProcessProviderName = "ProcessProvider" - -var ( - // ErrProcessProviderHomeNotFound is emitted when the user directory cannot be found. - ErrProcessProviderHomeNotFound = awserr.New("UserHomeNotFound", "user home directory not found.", nil) -) - -// A ProcessProvider retrieves credentials from a process specified as -// 'credential_process' in a profile, and keeps track if those credentials are -// expired. -// -// Profile ini file example: $HOME/.aws/config -type ProcessProvider struct { - Expiry - - // Path to the shared config file. - // - // If empty will look for "AWS_CONFIG_FILE" env variable. If the - // env value is empty will default to current user's home directory. - // Linux/OSX: "$HOME/.aws/config" - // Windows: "%USERPROFILE%\.aws\config" - Filename string - - // AWS Profile to retrieve the 'credential_process' from. If empty - // will default to environment variable "AWS_PROFILE" or "default" if - // environment variable is also not set. - Profile string - - // ExpiryWindow will allow the credentials to trigger refreshing prior to - // the credentials actually expiring. This is beneficial so race conditions - // with expiring credentials do not cause request to fail unexpectedly - // due to ExpiredTokenException exceptions. - // - // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true - // 10 seconds before the credentials are actually expired. - // - // If ExpiryWindow is 0 or less it will be ignored. - ExpiryWindow time.Duration - - // A function to perform the credential_process execution. Can be replaced - // for testing - executionFunc func(process string) ([]byte, error) -} - -// NewProcessProvider returns a pointer to a new Credentials object -// wrapping the Profile file provider. -func NewProcessProvider(filename, profile string) *Credentials { - return NewCredentials(&ProcessProvider{ - Filename: filename, - Profile: profile, - executionFunc: executeCredentialProcess, - }) -} - -// Retrieve executes the 'credential_process' and returns the credentials -func (p *ProcessProvider) Retrieve() (Value, error) { - filename, err := p.filename() - if err != nil { - return Value{ProviderName: ProcessProviderName}, err - } - - creds, err := p.loadProfile(filename, p.profile()) - if err != nil { - return Value{ProviderName: ProcessProviderName}, err - } - - return creds, nil -} - -// loadProfiles executes the 'credential_process' from the profile of the given -// file, and returns the credentials retrieved. An error will be returned if it -// fails to read from the file, or the data is invalid. -func (p *ProcessProvider) loadProfile(filename, profile string) (Value, error) { - config, err := ini.Load(filename) - if err != nil { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "failed to load shared config file", err) - } - - var iniProfile *ini.Section - if len(profile) == 0 || profile == "default" { - iniProfile, err = config.GetSection("default") - } else { - iniProfile, err = config.GetSection("profile " + profile) - } - if err != nil { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "failed to get profile", err) - } - - process, err := iniProfile.GetKey("credential_process") - if err != nil { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "No credential_process specified", err) - } - return p.credentialProcess(process.String()) -} - -type credentialProcessResponse struct { - Version int - AccessKeyID string `json:"AccessKeyId"` - SecretAccessKey string - SessionToken string - Expiration string -} - -func executeCredentialProcess(process string) ([]byte, error) { - processArgs := strings.Split(process, " ") - var command string - var cmdArgs []string - - if len(processArgs) == 1 { - command = processArgs[0] - } else if len(processArgs) > 1 { - command = processArgs[0] - cmdArgs = processArgs[1:] - } - - // TODO: Check for executability? - _, err := os.Stat(command) - if os.IsNotExist(err) { - return nil, awserr.New("ProcessProviderLoad", "credential_process "+command+" not found", err) - } else if err != nil { - return nil, awserr.New("ProcessProviderLoad", "failed to open credential_process "+command, err) - } - - // Execute command - cmd := exec.Command(command, cmdArgs...) - cmd.Env = os.Environ() - out, err := cmd.Output() - if err != nil { - return nil, awserr.New("ProcessProviderLoad", "Error executing credential_process", err) - } - return out, nil -} - -func (p *ProcessProvider) credentialProcess(process string) (Value, error) { - out, err := p.executionFunc(process) - if err != nil { - return Value{ProviderName: ProcessProviderName}, err - } - - // Serialize and validate response - resp := &credentialProcessResponse{} - err = json.Unmarshal(out, resp) - if err != nil { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "Error parsing credential_process output: "+string(out), err) - } - if resp.Version != 1 { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", " Version in credential_process output is not 1", err) - } - if len(resp.AccessKeyID) == 0 { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", " Missing required AccessKeyId in credential_process output", err) - } - if len(resp.SecretAccessKey) == 0 { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", " Missing required SecretAccessKey in credential_process output", err) - } - - // Handle expiration - if len(resp.Expiration) > 0 { - expiry, err := time.Parse(time.RFC3339, resp.Expiration) - if err != nil { - return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "Error parsing expiration of credential_process output: "+resp.Expiration, err) - } - p.SetExpiration(expiry, p.ExpiryWindow) - } - - return Value{ - ProviderName: ProcessProviderName, - AccessKeyID: resp.AccessKeyID, - SecretAccessKey: resp.SecretAccessKey, - SessionToken: resp.SessionToken, - }, nil - -} - -// filename returns the filename to use to read AWS shared credentials. -// -// Will return an error if the user's home directory path cannot be found. -func (p *ProcessProvider) filename() (string, error) { - if len(p.Filename) != 0 { - return p.Filename, nil - } - - if p.Filename = os.Getenv("AWS_CONFIG_FILE"); len(p.Filename) != 0 { - return p.Filename, nil - } - - if home := shareddefaults.UserHomeDir(); len(home) == 0 { - // Backwards compatibility of home directly not found error being returned. - // This error is too verbose, failure when opening the file would of been - // a better error to return. - return "", ErrProcessProviderHomeNotFound - } - - p.Filename = shareddefaults.SharedConfigFilename() - - return p.Filename, nil -} - -// profile returns the AWS shared config profile. If empty will read -// environment variable "AWS_PROFILE". If that is not set profile will -// return "default". -func (p *ProcessProvider) profile() string { - if p.Profile == "" { - p.Profile = os.Getenv("AWS_PROFILE") - } - if p.Profile == "" { - p.Profile = os.Getenv("AWS_DEFAULT_PROFILE") - } - if p.Profile == "" { - p.Profile = "default" - } - - return p.Profile -} diff --git a/aws/credentials/processcreds/provider.go b/aws/credentials/processcreds/provider.go new file mode 100644 index 00000000000..f7bb196be18 --- /dev/null +++ b/aws/credentials/processcreds/provider.go @@ -0,0 +1,421 @@ +/* +Package processcreds is a credential Provider to retrieve `credential_process` +credentials. + +**Warning:** +The following describes a method of sourcing credentials from an external +process. This can potentially be dangerous, so proceed with caution. Other +credential providers should be preferred if at all possible. If using this +option, you should make sure that the config file is as locked down as possible +using security best practices for your operating system. + +You can use credentials from a `credential_process` in a variety of ways. + +One way is to setup your shared config file, located in the default +location, with the `credential_process` key and the command you want to be +called. You also need to set the AWS_SDK_LOAD_CONFIG environment variable +(e.g., `export AWS_SDK_LOAD_CONFIG=1`) to use the shared config file. + + [default] + credential_process = /command/to/call + +Creating a new session will use the credential process to retrieve credentials. +NOTE: If there are credentials in the profile you are using, the credential +process will not be used. + + // Initialize a session to load credentials. + sess, _ := session.NewSession(&aws.Config{ + Region: aws.String("us-east-1")}, + ) + + // Create S3 service client to use the credentials. + svc := s3.New(sess) + +Another way to use the `credential_process` method is by using +`credentials.NewCredentials()` and providing a command to be executed to +retrieve credentials: + + // Create credentials using the ProcessProvider. + creds := processcreds.NewCredentials("/path/to/command") + + // Create service client value configured for credentials. + svc := s3.New(sess, &aws.Config{Credentials: creds}) + +You can set a non-default timeout for the `credential_process` with another +constructor, `credentials.NewCredentialsTimeout()`, providing the timeout. To +set a one minute timeout: + + // Create credentials using the ProcessProvider. + creds := processcreds.NewCredentialsTimeout( + "/path/to/command", + time.Duration(500) * time.Millisecond) + +If you need more control, you can set any configurable options in the +credentials using one or more option functions. For example, you can set a two +minute timeout, a credential duration of 60 minutes, and a maximum stdout +buffer size of 2k. + + creds := processcreds.NewCredentials( + "/path/to/command", + func(opt *ProcessProvider) { + opt.Timeout = time.Duration(2) * time.Minute + opt.Duration = time.Duration(60) * time.Minute + opt.MaxBufSize = 2048 + }) + +You can also use your own `exec.Cmd`: + + // Create an exec.Cmd + myCommand := exec.Command("/path/to/command") + + // Create credentials using the ProcessProvider. + creds := credentials.NewCredentials(&processcreds.ProcessProvider{ + Command: myCommand, + Timeout: time.Duration(2) * time.Minute, + MaxBufSize: 500, + }) +*/ +package processcreds + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" +) + +const ( + // ProviderName is the name this credentials provider will label any + // returned credentials Value with. + ProviderName = `ProcessProvider` + + // ErrCodeProcessProviderParse error parsing process output + ErrCodeProcessProviderParse = "ProcessProviderParseError" + + // ErrCodeProcessProviderVersion version error in output + ErrCodeProcessProviderVersion = "ProcessProviderVersionError" + + // ErrCodeProcessProviderRequired required attribute missing in output + ErrCodeProcessProviderRequired = "ProcessProviderRequiredError" + + // ErrCodeProcessProviderExecution execution of command failed + ErrCodeProcessProviderExecution = "ProcessProviderExecutionError" + + // ErrMsgProcessProviderTimeout process took longer than allowed + ErrMsgProcessProviderTimeout = "credential process timed out" + + // ErrMsgProcessProviderNoKill process could not be killed + ErrMsgProcessProviderNoKill = "unable to kill process" + + // ErrMsgProcessProviderProcess process error + ErrMsgProcessProviderProcess = "error in credential_process" + + // ErrMsgProcessProviderParse problem parsing output + ErrMsgProcessProviderParse = "parse failed of credential_process output" + + // ErrMsgProcessProviderVersion version error in output + ErrMsgProcessProviderVersion = "wrong version in process output (not 1)" + + // ErrMsgProcessProviderMissKey missing access key id in output + ErrMsgProcessProviderMissKey = "missing AccessKeyId in process output" + + // ErrMsgProcessProviderMissSecret missing secret acess key in output + ErrMsgProcessProviderMissSecret = "missing SecretAccessKey in process output" + + // ErrMsgProcessProviderPrepareCmd prepare of command failed + ErrMsgProcessProviderPrepareCmd = "failed to prepare command" + + // ErrMsgProcessProviderFewArgs not enough args + ErrMsgProcessProviderFewArgs = "not enough args" + + // ErrMsgProcessProviderEmptyCmd command must not be empty + ErrMsgProcessProviderEmptyCmd = "command must not be empty" + + // ErrMsgProcessProviderPipe failed to initialize pipe + ErrMsgProcessProviderPipe = "failed to initialize pipe" + + // GoOSWindowsKey for identifying Windows OS + GoOSWindowsKey = "windows" + + // DefaultDuration is the default amount of time in minutes that the + // credentials will be valid for. + DefaultDuration = time.Duration(15) * time.Minute + + // DefaultInitialBufSize size for initial buffer. + DefaultInitialBufSize = 200 + + // DefaultMaxBufSize limits buffer size from growing to an enormous + // amount due to a faulty process. + DefaultMaxBufSize = 512 + + // DefaultTimeout default limit on time a process can run. + DefaultTimeout = time.Duration(1) * time.Minute +) + +// ProcessProvider satisfies the credentials.Provider interface, and is a +// client to retrieve credentials from a process. +type ProcessProvider struct { + staticCreds bool + credentials.Expiry + originalCommand []string + + // Expiry duration of the credentials. Defaults to 15 minutes if not set. + Duration time.Duration + + // ExpiryWindow will allow the credentials to trigger refreshing prior to + // the credentials actually expiring. This is beneficial so race conditions + // with expiring credentials do not cause request to fail unexpectedly + // due to ExpiredTokenException exceptions. + // + // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // 10 seconds before the credentials are actually expired. + // + // If ExpiryWindow is 0 or less it will be ignored. + ExpiryWindow time.Duration + + // A string representing an os command that should return a JSON with + // credential information. + Command *exec.Cmd + + // MaxBufSize limits memory usage from growing to an enormous + // amount due to a faulty process. + MaxBufSize int + + // Timeout limits the time a process can run. + Timeout time.Duration +} + +// NewCredentials returns a pointer to a new Credentials object wrapping the +// ProcessProvider. The credentials will expire every 15 minutes by default. +func NewCredentials(command string, options ...func(*ProcessProvider)) *credentials.Credentials { + p := &ProcessProvider{ + Command: exec.Command(command), + Duration: DefaultDuration, + Timeout: DefaultTimeout, + MaxBufSize: DefaultMaxBufSize, + } + + for _, option := range options { + option(p) + } + + return credentials.NewCredentials(p) +} + +// NewCredentialsTimeout returns a pointer to a new Credentials object with +// the specified command and timeout, and default duration and max buffer size. +func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.Credentials { + p := NewCredentials(command, func(opt *ProcessProvider) { + opt.Timeout = timeout + }) + + return p +} + +type credentialProcessResponse struct { + Version int + AccessKeyID string `json:"AccessKeyId"` + SecretAccessKey string + SessionToken string + Expiration *time.Time +} + +// Retrieve executes the 'credential_process' and returns the credentials. +func (p *ProcessProvider) Retrieve() (credentials.Value, error) { + out, err := p.executeCredentialProcess() + if err != nil { + return credentials.Value{ProviderName: ProviderName}, err + } + + // Serialize and validate response + resp := &credentialProcessResponse{} + if err = json.Unmarshal(out, resp); err != nil { + return credentials.Value{ProviderName: ProviderName}, awserr.New( + ErrCodeProcessProviderParse, + fmt.Sprintf("%s: %s", ErrMsgProcessProviderParse, string(out)), + err) + } + + if resp.Version != 1 { + return credentials.Value{ProviderName: ProviderName}, awserr.New( + ErrCodeProcessProviderVersion, + ErrMsgProcessProviderVersion, + nil) + } + + if len(resp.AccessKeyID) == 0 { + return credentials.Value{ProviderName: ProviderName}, awserr.New( + ErrCodeProcessProviderRequired, + ErrMsgProcessProviderMissKey, + nil) + } + + if len(resp.SecretAccessKey) == 0 { + return credentials.Value{ProviderName: ProviderName}, awserr.New( + ErrCodeProcessProviderRequired, + ErrMsgProcessProviderMissSecret, + nil) + } + + // Handle expiration + p.staticCreds = resp.Expiration == nil + if resp.Expiration != nil { + p.SetExpiration(*resp.Expiration, p.ExpiryWindow) + } + + return credentials.Value{ + ProviderName: ProviderName, + AccessKeyID: resp.AccessKeyID, + SecretAccessKey: resp.SecretAccessKey, + SessionToken: resp.SessionToken, + }, nil +} + +// IsExpired returns true if the credentials retrieved are expired, or not yet +// retrieved. +func (p *ProcessProvider) IsExpired() bool { + if p.staticCreds { + return false + } + return p.Expiry.IsExpired() +} + +// prepareCommand prepares the command to be executed. +func (p *ProcessProvider) prepareCommand() error { + + var cmdArgs []string + if runtime.GOOS == GoOSWindowsKey { + cmdArgs = []string{"cmd.exe", "/C"} + } else { + cmdArgs = []string{"sh", "-c"} + } + + if len(p.originalCommand) == 0 { + p.originalCommand = make([]string, len(p.Command.Args)) + copy(p.originalCommand, p.Command.Args) + + // check for empty command because it succeeds + if len(strings.TrimSpace(p.originalCommand[0])) < 1 { + return awserr.New( + ErrCodeProcessProviderExecution, + fmt.Sprintf( + "%s: %s", + ErrMsgProcessProviderPrepareCmd, + ErrMsgProcessProviderEmptyCmd), + nil) + } + } + + cmdArgs = append(cmdArgs, p.originalCommand...) + p.Command = exec.Command(cmdArgs[0], cmdArgs[1:]...) + p.Command.Env = os.Environ() + + return nil +} + +// executeCredentialProcess starts the credential process on the OS and +// returns the results or an error. +func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) { + + if err := p.prepareCommand(); err != nil { + return nil, err + } + + // Setup the pipes + outReadPipe, outWritePipe, err := os.Pipe() + if err != nil { + return nil, awserr.New( + ErrCodeProcessProviderExecution, + ErrMsgProcessProviderPipe, + err) + } + + p.Command.Stderr = os.Stderr // display stderr on console for MFA + p.Command.Stdout = outWritePipe // get creds json on process's stdout + p.Command.Stdin = os.Stdin // enable stdin for MFA + + output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize)) + + stdoutCh := make(chan error, 1) + go readInput( + io.LimitReader(outReadPipe, int64(p.MaxBufSize)), + output, + stdoutCh) + + execCh := make(chan error, 1) + go executeCommand(*p.Command, execCh) + + finished := false + var errors []error + for !finished { + select { + case readError := <-stdoutCh: + errors = appendError(errors, readError) + finished = true + case execError := <-execCh: + err := outWritePipe.Close() + errors = appendError(errors, err) + errors = appendError(errors, execError) + if errors != nil { + return output.Bytes(), awserr.NewBatchError( + ErrCodeProcessProviderExecution, + ErrMsgProcessProviderProcess, + errors) + } + case <-time.After(p.Timeout): + finished = true + return output.Bytes(), awserr.NewBatchError( + ErrCodeProcessProviderExecution, + ErrMsgProcessProviderTimeout, + errors) // errors can be nil + } + } + + out := output.Bytes() + + if runtime.GOOS == GoOSWindowsKey { + // windows adds slashes to quotes + out = []byte(strings.Replace(string(out), `\"`, `"`, -1)) + } + + return out, nil +} + +// appendError conveniently checks for nil before appending slice +func appendError(errors []error, err error) []error { + if err != nil { + return append(errors, err) + } + return errors +} + +func executeCommand(cmd exec.Cmd, exec chan error) { + // Start the command + err := cmd.Start() + if err == nil { + err = cmd.Wait() + } + + exec <- err +} + +func readInput(r io.Reader, w io.Writer, read chan error) { + tee := io.TeeReader(r, w) + + _, err := ioutil.ReadAll(tee) + + if err == io.EOF { + err = nil + } + + read <- err // will only arrive here when write end of pipe is closed +} diff --git a/aws/credentials/process_provider_test.go b/aws/credentials/processcreds/provider_test.go similarity index 99% rename from aws/credentials/process_provider_test.go rename to aws/credentials/processcreds/provider_test.go index a273cb59b41..b2ed5b36492 100644 --- a/aws/credentials/process_provider_test.go +++ b/aws/credentials/processcreds/provider_test.go @@ -1,4 +1,4 @@ -package credentials +package processcreds import ( "os" diff --git a/aws/defaults/defaults.go b/aws/defaults/defaults.go index 2dddf139b78..23bb639e018 100644 --- a/aws/defaults/defaults.go +++ b/aws/defaults/defaults.go @@ -93,12 +93,7 @@ func Handlers() request.Handlers { func CredChain(cfg *aws.Config, handlers request.Handlers) *credentials.Credentials { return credentials.NewCredentials(&credentials.ChainProvider{ VerboseErrors: aws.BoolValue(cfg.CredentialsChainVerboseErrors), - Providers: []credentials.Provider{ - &credentials.EnvProvider{}, - &credentials.SharedCredentialsProvider{Filename: "", Profile: ""}, - &credentials.ProcessProvider{Filename: "", Profile: ""}, - RemoteCredProvider(*cfg, handlers), - }, + Providers: CredProviders(cfg, handlers), }) } diff --git a/aws/session/session.go b/aws/session/session.go index 2d929be54a9..9bdbafd65cc 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/corehandlers" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/processcreds" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/csm" "github.com/aws/aws-sdk-go/aws/defaults" @@ -534,10 +535,9 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg share cfg.Credentials = credentials.NewStaticCredentialsFromCreds( sharedCfg.Creds, ) - } else if envCfg.EnableSharedConfig && len(sharedCfg.CredentialProcess) > 0 { - cfg.Credentials = credentials.NewProcessProvider( - envCfg.SharedConfigFile, - envCfg.Profile, + } else if len(sharedCfg.CredentialProcess) > 0 { + cfg.Credentials = processcreds.NewCredentials( + sharedCfg.CredentialProcess, ) } else { // Fallback to default credentials provider, include mock errors diff --git a/aws/session/shared_config.go b/aws/session/shared_config.go index bf9fe3f9748..7cb44021b3f 100644 --- a/aws/session/shared_config.go +++ b/aws/session/shared_config.go @@ -228,9 +228,9 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile) e } } - credentialProcess := section.Key(credentialProcessKey).String() - if len(credentialProcess) > 0 { - cfg.CredentialProcess = credentialProcess + // `credential_process` + if credProc := section.String(credentialProcessKey); len(credProc) > 0 { + cfg.CredentialProcess = credProc } // Region From fe47cc9c3985964632e82ea539c675de23016b09 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Wed, 17 Oct 2018 15:38:42 -0400 Subject: [PATCH 3/5] aws/credentials: Add tests for credential_process --- aws/credentials/example.ini | 4 - aws/credentials/processcreds/provider_test.go | 555 +++++++++++++++--- .../processcreds/testdata/expired.json | 7 + .../processcreds/testdata/malformed.json | 2 + .../processcreds/testdata/missingkey.json | 4 + .../processcreds/testdata/missingsecret.json | 4 + .../processcreds/testdata/nonexpire.json | 6 + .../processcreds/testdata/shconfig.ini | 10 + .../processcreds/testdata/shconfig_win.ini | 10 + .../processcreds/testdata/shcred.ini | 10 + .../processcreds/testdata/shcred_win.ini | 10 + .../processcreds/testdata/static.json | 5 + .../processcreds/testdata/verybad.json | 5 + .../processcreds/testdata/wrongversion.json | 3 + 14 files changed, 547 insertions(+), 88 deletions(-) create mode 100644 aws/credentials/processcreds/testdata/expired.json create mode 100644 aws/credentials/processcreds/testdata/malformed.json create mode 100644 aws/credentials/processcreds/testdata/missingkey.json create mode 100644 aws/credentials/processcreds/testdata/missingsecret.json create mode 100644 aws/credentials/processcreds/testdata/nonexpire.json create mode 100644 aws/credentials/processcreds/testdata/shconfig.ini create mode 100644 aws/credentials/processcreds/testdata/shconfig_win.ini create mode 100644 aws/credentials/processcreds/testdata/shcred.ini create mode 100644 aws/credentials/processcreds/testdata/shcred_win.ini create mode 100644 aws/credentials/processcreds/testdata/static.json create mode 100644 aws/credentials/processcreds/testdata/verybad.json create mode 100644 aws/credentials/processcreds/testdata/wrongversion.json diff --git a/aws/credentials/example.ini b/aws/credentials/example.ini index 84a3536a4f0..7fc91d9d204 100644 --- a/aws/credentials/example.ini +++ b/aws/credentials/example.ini @@ -2,7 +2,6 @@ aws_access_key_id = accessKey aws_secret_access_key = secret aws_session_token = token -credential_process = /bin/echo {"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": "2000-01-01T00:00:00-00:00"} [no_token] aws_access_key_id = accessKey @@ -11,6 +10,3 @@ aws_secret_access_key = secret [with_colon] aws_access_key_id: accessKey aws_secret_access_key: secret - -[profile process] -credential_process = /bin/echo {"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenProcess", "Expiration": "2000-01-01T00:00:00-00:00"} diff --git a/aws/credentials/processcreds/provider_test.go b/aws/credentials/processcreds/provider_test.go index b2ed5b36492..22ae7d72fc3 100644 --- a/aws/credentials/processcreds/provider_test.go +++ b/aws/credentials/processcreds/provider_test.go @@ -1,143 +1,530 @@ -package processcreds +package processcreds_test import ( + "encoding/json" + "fmt" + "io/ioutil" "os" - "path/filepath" + "runtime" + "strings" "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials/processcreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/awstesting" ) -func TestProcessProvider(t *testing.T) { - os.Clearenv() +func TestProcessProviderFromSessionCfg(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) - p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: executeCredentialProcess} - creds, err := p.Retrieve() - assert.Nil(t, err, "Expect no error") + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + if runtime.GOOS == "windows" { + os.Setenv("AWS_CONFIG_FILE", "testdata\\shconfig_win.ini") + } else { + os.Setenv("AWS_CONFIG_FILE", "testdata/shconfig.ini") + } - assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") - assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") - assert.Equal(t, "tokenProcess", creds.SessionToken, "Expect session token to match") -} + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("region")}, + ) + + if err != nil { + t.Errorf("error getting session: %v", err) + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("error getting credentials: %v", err) + } + + if e, a := "accessKey", creds.AccessKeyID; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + if e, a := "secret", creds.SecretAccessKey; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + if e, a := "tokenDefault", creds.SessionToken; e != a { + t.Errorf("expected %v, got %v", e, a) + } -func fakeExectuteCredsExpired(process string) ([]byte, error) { - return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": "2000-01-01T00:00:00-00:00"}`), nil } -func TestProcessProviderIsExpired(t *testing.T) { - os.Clearenv() +func TestProcessProviderFromSessionWithProfileCfg(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) - p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsExpired} + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_PROFILE", "non_expire") + if runtime.GOOS == "windows" { + os.Setenv("AWS_CONFIG_FILE", "testdata\\shconfig_win.ini") + } else { + os.Setenv("AWS_CONFIG_FILE", "testdata/shconfig.ini") + } + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("region")}, + ) + + if err != nil { + t.Errorf("error getting session: %v", err) + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("error getting credentials: %v", err) + } + + if e, a := "nonDefaultToken", creds.SessionToken; e != a { + t.Errorf("expected %v, got %v", e, a) + } - assert.True(t, p.IsExpired(), "Expect creds to be expired before retrieve") } -func TestProcessProviderWithAWS_CONFIG_FILE(t *testing.T) { - os.Clearenv() - os.Setenv("AWS_CONFIG_FILE", "example.ini") - os.Setenv("AWS_DEFAULT_PROFILE", "process") - p := ProcessProvider{Filename: "", Profile: "", executionFunc: executeCredentialProcess} - creds, err := p.Retrieve() +func TestProcessProviderNotFromCredProcCfg(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_PROFILE", "not_alone") + if runtime.GOOS == "windows" { + os.Setenv("AWS_CONFIG_FILE", "testdata\\shconfig_win.ini") + } else { + os.Setenv("AWS_CONFIG_FILE", "testdata/shconfig.ini") + } + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("region")}, + ) + + if err != nil { + t.Errorf("error getting session: %v", err) + } - assert.Nil(t, err, "Expect no error") + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("error getting credentials: %v", err) + } + + if e, a := "notFromCredProcAccess", creds.AccessKeyID; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + if e, a := "notFromCredProcSecret", creds.SecretAccessKey; e != a { + t.Errorf("expected %v, got %v", e, a) + } - assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") - assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") - assert.Equal(t, "tokenProcess", creds.SessionToken, "Expect session token to match") } -func TestProcessProviderWithAWS_CONFIG_FILEAbsPath(t *testing.T) { - os.Clearenv() - wd, err := os.Getwd() - assert.NoError(t, err) - os.Setenv("AWS_CONFIG_FILE", filepath.Join(wd, "example.ini")) - p := ProcessProvider{executionFunc: executeCredentialProcess} - creds, err := p.Retrieve() - assert.Nil(t, err, "Expect no error") +func TestProcessProviderFromSessionCrd(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + if runtime.GOOS == "windows" { + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata\\shcred_win.ini") + } else { + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/shcred.ini") + } + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("region")}, + ) + + if err != nil { + t.Errorf("error getting session: %v", err) + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("error getting credentials: %v", err) + } + + if e, a := "accessKey", creds.AccessKeyID; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + if e, a := "secret", creds.SecretAccessKey; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + if e, a := "tokenDefault", creds.SessionToken; e != a { + t.Errorf("expected %v, got %v", e, a) + } - assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") - assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") - assert.Equal(t, "tokenDefault", creds.SessionToken, "Expect session token to match") } -func fakeExectuteCredsSuccess(process string) ([]byte, error) { - return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenFake", "Expiration": "2000-01-01T00:00:00-00:00"}`), nil +func TestProcessProviderFromSessionWithProfileCrd(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + os.Setenv("AWS_PROFILE", "non_expire") + if runtime.GOOS == "windows" { + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata\\shcred_win.ini") + } else { + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/shcred.ini") + } + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("region")}, + ) + + if err != nil { + t.Errorf("error getting session: %v", err) + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("error getting credentials: %v", err) + } + + if e, a := "nonDefaultToken", creds.SessionToken; e != a { + t.Errorf("expected %v, got %v", e, a) + } + } -func TestProcessProviderWithAWS_PROFILE(t *testing.T) { - os.Clearenv() - os.Setenv("AWS_PROFILE", "process") +func TestProcessProviderNotFromCredProcCrd(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + os.Setenv("AWS_PROFILE", "not_alone") + if runtime.GOOS == "windows" { + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata\\shcred_win.ini") + } else { + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/shcred.ini") + } + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("region")}, + ) + + if err != nil { + t.Errorf("error getting session: %v", err) + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("error getting credentials: %v", err) + } + + if e, a := "notFromCredProcAccess", creds.AccessKeyID; e != a { + t.Errorf("expected %v, got %v", e, a) + } - p := ProcessProvider{Filename: "example.ini", Profile: "", executionFunc: fakeExectuteCredsSuccess} - creds, err := p.Retrieve() - assert.Nil(t, err, "Expect no error") + if e, a := "notFromCredProcSecret", creds.SecretAccessKey; e != a { + t.Errorf("expected %v, got %v", e, a) + } - assert.Equal(t, "accessKey", creds.AccessKeyID, "Expect access key ID to match") - assert.Equal(t, "secret", creds.SecretAccessKey, "Expect secret access key to match") - assert.Equal(t, "tokenFake", creds.SessionToken, "Expect token to match") } -func fakeExectuteCredsFailMalformed(process string) ([]byte, error) { - return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": `), nil +func TestProcessProviderBadCommand(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + creds := processcreds.NewCredentials("/bad/process") + _, err := creds.Get() + if err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderExecution { + t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderExecution, err) + } } -func TestProcessProviderMalformed(t *testing.T) { - os.Clearenv() - os.Setenv("AWS_PROFILE", "process") +func TestProcessProviderMoreEmptyCommands(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + creds := processcreds.NewCredentials("") + _, err := creds.Get() + if err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderExecution { + t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderExecution, err) + } - p := ProcessProvider{Filename: "example.ini", Profile: "", executionFunc: fakeExectuteCredsFailMalformed} - _, err := p.Retrieve() - assert.NotNil(t, err, "Expect an error") } -func fakeExectuteCredsNoToken(process string) ([]byte, error) { - return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret"}`), nil +func TestProcessProviderExpectErrors(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + creds := processcreds.NewCredentials( + fmt.Sprintf( + "%s %s", + getOSCat(), + strings.Join( + []string{"testdata", "malformed.json"}, + string(os.PathSeparator)))) + _, err := creds.Get() + if err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderParse { + t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderParse, err) + } + + creds = processcreds.NewCredentials( + fmt.Sprintf("%s %s", + getOSCat(), + strings.Join( + []string{"testdata", "wrongversion.json"}, + string(os.PathSeparator)))) + _, err = creds.Get() + if err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderVersion { + t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderVersion, err) + } + + creds = processcreds.NewCredentials( + fmt.Sprintf( + "%s %s", + getOSCat(), + strings.Join( + []string{"testdata", "missingkey.json"}, + string(os.PathSeparator)))) + _, err = creds.Get() + if err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderRequired { + t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderRequired, err) + } + + creds = processcreds.NewCredentials( + fmt.Sprintf( + "%s %s", + getOSCat(), + strings.Join( + []string{"testdata", "missingsecret.json"}, + string(os.PathSeparator)))) + _, err = creds.Get() + if err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderRequired { + t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderRequired, err) + } + } -func TestProcessProviderNoToken(t *testing.T) { - os.Clearenv() +func TestProcessProviderTimeout(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + command := "/bin/sleep 2" + if runtime.GOOS == "windows" { + // "timeout" command does not work due to pipe redirection + command = "C:\\Windows\\system32\\ping -n 2 127.0.0.1>nul" + } + + creds := processcreds.NewCredentialsTimeout( + command, + time.Duration(1)*time.Second) + if _, err := creds.Get(); err == nil || err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderExecution || err.(awserr.Error).Message() != processcreds.ErrMsgProcessProviderTimeout { + t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderExecution, err) + } - p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsNoToken} - creds, err := p.Retrieve() - assert.Nil(t, err, "Expect no error") - assert.Empty(t, creds.SessionToken, "Expect no token") } -func fakeExectuteCredsFailVersion(process string) ([]byte, error) { - return []byte(`{"Version": 2, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault"}`), nil +type credentialTest struct { + Version int + AccessKeyID string `json:"AccessKeyId"` + SecretAccessKey string + Expiration string } -func TestProcessProviderWrongVersion(t *testing.T) { - os.Clearenv() - p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsFailVersion} - _, err := p.Retrieve() - assert.NotNil(t, err, "Expect an error") +func TestProcessProviderStatic(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + // static + creds := processcreds.NewCredentials( + fmt.Sprintf( + "%s %s", + getOSCat(), + strings.Join( + []string{"testdata", "static.json"}, + string(os.PathSeparator)))) + _, err := creds.Get() + if err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + if creds.IsExpired() { + t.Errorf("expected %v, got %v", "static credentials/not expired", "expired") + } + } -func fakeExectuteCredsFailExpiration(process string) ([]byte, error) { - return []byte(`{"Version": 1, "AccessKeyId": "accessKey", "SecretAccessKey": "secret", "SessionToken": "tokenDefault", "Expiration": "20222"}`), nil +func TestProcessProviderNotExpired(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + // non-static, not expired + exp := &credentialTest{} + exp.Version = 1 + exp.AccessKeyID = "accesskey" + exp.SecretAccessKey = "secretkey" + exp.Expiration = time.Now().Add(1 * time.Hour).UTC().Format(time.RFC3339) + b, err := json.Marshal(exp) + if err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + + tmpFile := strings.Join( + []string{"testdata", "tmp_expiring.json"}, + string(os.PathSeparator)) + if err = ioutil.WriteFile(tmpFile, b, 0644); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + defer func() { + if err = os.Remove(tmpFile); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + }() + creds := processcreds.NewCredentials( + fmt.Sprintf("%s %s", getOSCat(), tmpFile)) + _, err = creds.Get() + if err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + if creds.IsExpired() { + t.Errorf("expected %v, got %v", "not expired", "expired") + } } -func TestProcessProviderBadExpiry(t *testing.T) { - os.Clearenv() - p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: fakeExectuteCredsFailExpiration} - _, err := p.Retrieve() - assert.NotNil(t, err, "Expect an error") + +func TestProcessProviderExpired(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + // non-static, expired + exp := &credentialTest{} + exp.Version = 1 + exp.AccessKeyID = "accesskey" + exp.SecretAccessKey = "secretkey" + exp.Expiration = time.Now().Add(-1 * time.Hour).UTC().Format(time.RFC3339) + b, err := json.Marshal(exp) + if err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + + tmpFile := strings.Join( + []string{"testdata", "tmp_expired.json"}, + string(os.PathSeparator)) + if err = ioutil.WriteFile(tmpFile, b, 0644); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + defer func() { + if err = os.Remove(tmpFile); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + }() + creds := processcreds.NewCredentials( + fmt.Sprintf("%s %s", getOSCat(), tmpFile)) + _, err = creds.Get() + if err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + if !creds.IsExpired() { + t.Errorf("expected %v, got %v", "expired", "not expired") + } +} + +func TestProcessProviderForceExpire(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + // non-static, not expired + + // setup test credentials file + exp := &credentialTest{} + exp.Version = 1 + exp.AccessKeyID = "accesskey" + exp.SecretAccessKey = "secretkey" + exp.Expiration = time.Now().Add(1 * time.Hour).UTC().Format(time.RFC3339) + b, err := json.Marshal(exp) + if err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + tmpFile := strings.Join( + []string{"testdata", "tmp_force_expire.json"}, + string(os.PathSeparator)) + if err = ioutil.WriteFile(tmpFile, b, 0644); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + defer func() { + if err = os.Remove(tmpFile); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + }() + + // get credentials from file + creds := processcreds.NewCredentials( + fmt.Sprintf("%s %s", getOSCat(), tmpFile)) + if _, err = creds.Get(); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + if creds.IsExpired() { + t.Errorf("expected %v, got %v", "not expired", "expired") + } + + // force expire creds + creds.Expire() + if !creds.IsExpired() { + t.Errorf("expected %v, got %v", "expired", "not expired") + } + + // renew creds + if _, err = creds.Get(); err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + if creds.IsExpired() { + t.Errorf("expected %v, got %v", "not expired", "expired") + } + } func BenchmarkProcessProvider(b *testing.B) { - os.Clearenv() + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) - p := ProcessProvider{Filename: "example.ini", Profile: "process", executionFunc: executeCredentialProcess} - _, err := p.Retrieve() + creds := processcreds.NewCredentials( + fmt.Sprintf( + "%s %s", + getOSCat(), + strings.Join( + []string{"testdata", "static.json"}, + string(os.PathSeparator)))) + _, err := creds.Get() if err != nil { b.Fatal(err) } b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := p.Retrieve() + _, err := creds.Get() if err != nil { b.Fatal(err) } } } + +func preserveImportantStashEnv() []string { + extraEnv := make(map[string]string) + if runtime.GOOS == "windows" { + key := "ComSpec" + if val, ok := os.LookupEnv(key); ok && len(val) > 0 { + extraEnv[key] = val + } + } + + key := "PATH" + if val, ok := os.LookupEnv(key); ok && len(val) > 0 { + extraEnv[key] = val + } + + oldEnv := awstesting.StashEnv() //clear env + + for key, val := range extraEnv { + os.Setenv(key, val) + } + + return oldEnv +} + +func getOSCat() string { + if runtime.GOOS == "windows" { + return "type" + } + return "cat" +} diff --git a/aws/credentials/processcreds/testdata/expired.json b/aws/credentials/processcreds/testdata/expired.json new file mode 100644 index 00000000000..00753a8d12b --- /dev/null +++ b/aws/credentials/processcreds/testdata/expired.json @@ -0,0 +1,7 @@ +{ + "Version": 1, + "AccessKeyId": "accessKey", + "SecretAccessKey": "secret", + "SessionToken": "tokenDefault", + "Expiration": "2000-01-01T00:00:00-00:00" +} diff --git a/aws/credentials/processcreds/testdata/malformed.json b/aws/credentials/processcreds/testdata/malformed.json new file mode 100644 index 00000000000..1e9652b423d --- /dev/null +++ b/aws/credentials/processcreds/testdata/malformed.json @@ -0,0 +1,2 @@ +{ + "Version": 1 diff --git a/aws/credentials/processcreds/testdata/missingkey.json b/aws/credentials/processcreds/testdata/missingkey.json new file mode 100644 index 00000000000..ea54b015536 --- /dev/null +++ b/aws/credentials/processcreds/testdata/missingkey.json @@ -0,0 +1,4 @@ +{ + "Version": 1, + "AccessKeyId": "accesskey" +} diff --git a/aws/credentials/processcreds/testdata/missingsecret.json b/aws/credentials/processcreds/testdata/missingsecret.json new file mode 100644 index 00000000000..c8740b13f4d --- /dev/null +++ b/aws/credentials/processcreds/testdata/missingsecret.json @@ -0,0 +1,4 @@ +{ + "Version": 1, + "SecretAccessKey": "secretkey" +} diff --git a/aws/credentials/processcreds/testdata/nonexpire.json b/aws/credentials/processcreds/testdata/nonexpire.json new file mode 100644 index 00000000000..5e567131a47 --- /dev/null +++ b/aws/credentials/processcreds/testdata/nonexpire.json @@ -0,0 +1,6 @@ +{ + "Version": 1, + "AccessKeyId": "accessKey", + "SecretAccessKey": "secret", + "SessionToken": "nonDefaultToken" +} diff --git a/aws/credentials/processcreds/testdata/shconfig.ini b/aws/credentials/processcreds/testdata/shconfig.ini new file mode 100644 index 00000000000..9c236946c2b --- /dev/null +++ b/aws/credentials/processcreds/testdata/shconfig.ini @@ -0,0 +1,10 @@ +[default] +credential_process = cat ./testdata/expired.json + +[profile non_expire] +credential_process = cat ./testdata/nonexpire.json + +[profile not_alone] +aws_access_key_id = notFromCredProcAccess +aws_secret_access_key = notFromCredProcSecret +credential_process = cat ./testdata/verybad.json diff --git a/aws/credentials/processcreds/testdata/shconfig_win.ini b/aws/credentials/processcreds/testdata/shconfig_win.ini new file mode 100644 index 00000000000..59318d88e54 --- /dev/null +++ b/aws/credentials/processcreds/testdata/shconfig_win.ini @@ -0,0 +1,10 @@ +[default] +credential_process = type .\testdata\expired.json + +[profile non_expire] +credential_process = type .\testdata\nonexpire.json + +[profile not_alone] +aws_access_key_id = notFromCredProcAccess +aws_secret_access_key = notFromCredProcSecret +credential_process = type .\testdata\verybad.json diff --git a/aws/credentials/processcreds/testdata/shcred.ini b/aws/credentials/processcreds/testdata/shcred.ini new file mode 100644 index 00000000000..81ca26ba960 --- /dev/null +++ b/aws/credentials/processcreds/testdata/shcred.ini @@ -0,0 +1,10 @@ +[default] +credential_process = cat ./testdata/expired.json + +[non_expire] +credential_process = cat ./testdata/nonexpire.json + +[not_alone] +aws_access_key_id = notFromCredProcAccess +aws_secret_access_key = notFromCredProcSecret +credential_process = cat ./testdata/verybad.json diff --git a/aws/credentials/processcreds/testdata/shcred_win.ini b/aws/credentials/processcreds/testdata/shcred_win.ini new file mode 100644 index 00000000000..ad4559c258d --- /dev/null +++ b/aws/credentials/processcreds/testdata/shcred_win.ini @@ -0,0 +1,10 @@ +[default] +credential_process = type .\testdata\expired.json + +[non_expire] +credential_process = type .\testdata\nonexpire.json + +[not_alone] +aws_access_key_id = notFromCredProcAccess +aws_secret_access_key = notFromCredProcSecret +credential_process = type .\testdata\verybad.json diff --git a/aws/credentials/processcreds/testdata/static.json b/aws/credentials/processcreds/testdata/static.json new file mode 100644 index 00000000000..9fddfa123fc --- /dev/null +++ b/aws/credentials/processcreds/testdata/static.json @@ -0,0 +1,5 @@ +{ + "Version":1, + "AccessKeyId":"accesskey", + "SecretAccessKey":"secretkey" +} diff --git a/aws/credentials/processcreds/testdata/verybad.json b/aws/credentials/processcreds/testdata/verybad.json new file mode 100644 index 00000000000..968883b8b80 --- /dev/null +++ b/aws/credentials/processcreds/testdata/verybad.json @@ -0,0 +1,5 @@ +{ + "Version":1, + "AccessKeyId":"veryBadAccessKeyID", + "SecretAccessKey":"veryBadSecretAccessKey" +} diff --git a/aws/credentials/processcreds/testdata/wrongversion.json b/aws/credentials/processcreds/testdata/wrongversion.json new file mode 100644 index 00000000000..a58ea78dcef --- /dev/null +++ b/aws/credentials/processcreds/testdata/wrongversion.json @@ -0,0 +1,3 @@ +{ + "Version": 2 +} From 8771fbcd56fc47ae9d1382467694be6aff8a266d Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Mon, 26 Nov 2018 23:59:15 -0500 Subject: [PATCH 4/5] Fix credential process based on review --- aws/credentials/processcreds/provider.go | 132 ++++++++++++----------- 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/aws/credentials/processcreds/provider.go b/aws/credentials/processcreds/provider.go index f7bb196be18..f56e70c7392 100644 --- a/aws/credentials/processcreds/provider.go +++ b/aws/credentials/processcreds/provider.go @@ -2,8 +2,7 @@ Package processcreds is a credential Provider to retrieve `credential_process` credentials. -**Warning:** -The following describes a method of sourcing credentials from an external +WARNING: The following describes a method of sourcing credentials from an external process. This can potentially be dangerous, so proceed with caution. Other credential providers should be preferred if at all possible. If using this option, you should make sure that the config file is as locked down as possible @@ -65,15 +64,15 @@ buffer size of 2k. You can also use your own `exec.Cmd`: - // Create an exec.Cmd - myCommand := exec.Command("/path/to/command") + // Create an exec.Cmd + myCommand := exec.Command("/path/to/command") - // Create credentials using the ProcessProvider. - creds := credentials.NewCredentials(&processcreds.ProcessProvider{ - Command: myCommand, - Timeout: time.Duration(2) * time.Minute, - MaxBufSize: 500, - }) + // Create credentials using your exec.Cmd and custom timeout + creds := processcreds.NewCredentialsCommand( + myCommand, + func(opt *processcreds.ProcessProvider) { + opt.Timeout = time.Duration(1) * time.Second + }) */ package processcreds @@ -110,52 +109,40 @@ const ( // ErrCodeProcessProviderExecution execution of command failed ErrCodeProcessProviderExecution = "ProcessProviderExecutionError" - // ErrMsgProcessProviderTimeout process took longer than allowed - ErrMsgProcessProviderTimeout = "credential process timed out" - - // ErrMsgProcessProviderNoKill process could not be killed - ErrMsgProcessProviderNoKill = "unable to kill process" - - // ErrMsgProcessProviderProcess process error - ErrMsgProcessProviderProcess = "error in credential_process" + // errMsgProcessProviderTimeout process took longer than allowed + errMsgProcessProviderTimeout = "credential process timed out" - // ErrMsgProcessProviderParse problem parsing output - ErrMsgProcessProviderParse = "parse failed of credential_process output" + // errMsgProcessProviderProcess process error + errMsgProcessProviderProcess = "error in credential_process" - // ErrMsgProcessProviderVersion version error in output - ErrMsgProcessProviderVersion = "wrong version in process output (not 1)" + // errMsgProcessProviderParse problem parsing output + errMsgProcessProviderParse = "parse failed of credential_process output" - // ErrMsgProcessProviderMissKey missing access key id in output - ErrMsgProcessProviderMissKey = "missing AccessKeyId in process output" + // errMsgProcessProviderVersion version error in output + errMsgProcessProviderVersion = "wrong version in process output (not 1)" - // ErrMsgProcessProviderMissSecret missing secret acess key in output - ErrMsgProcessProviderMissSecret = "missing SecretAccessKey in process output" + // errMsgProcessProviderMissKey missing access key id in output + errMsgProcessProviderMissKey = "missing AccessKeyId in process output" - // ErrMsgProcessProviderPrepareCmd prepare of command failed - ErrMsgProcessProviderPrepareCmd = "failed to prepare command" + // errMsgProcessProviderMissSecret missing secret acess key in output + errMsgProcessProviderMissSecret = "missing SecretAccessKey in process output" - // ErrMsgProcessProviderFewArgs not enough args - ErrMsgProcessProviderFewArgs = "not enough args" + // errMsgProcessProviderPrepareCmd prepare of command failed + errMsgProcessProviderPrepareCmd = "failed to prepare command" - // ErrMsgProcessProviderEmptyCmd command must not be empty - ErrMsgProcessProviderEmptyCmd = "command must not be empty" + // errMsgProcessProviderEmptyCmd command must not be empty + errMsgProcessProviderEmptyCmd = "command must not be empty" - // ErrMsgProcessProviderPipe failed to initialize pipe - ErrMsgProcessProviderPipe = "failed to initialize pipe" - - // GoOSWindowsKey for identifying Windows OS - GoOSWindowsKey = "windows" + // errMsgProcessProviderPipe failed to initialize pipe + errMsgProcessProviderPipe = "failed to initialize pipe" // DefaultDuration is the default amount of time in minutes that the // credentials will be valid for. DefaultDuration = time.Duration(15) * time.Minute - // DefaultInitialBufSize size for initial buffer. - DefaultInitialBufSize = 200 - - // DefaultMaxBufSize limits buffer size from growing to an enormous + // DefaultBufSize limits buffer size from growing to an enormous // amount due to a faulty process. - DefaultMaxBufSize = 512 + DefaultBufSize = 512 // DefaultTimeout default limit on time a process can run. DefaultTimeout = time.Duration(1) * time.Minute @@ -184,7 +171,7 @@ type ProcessProvider struct { // A string representing an os command that should return a JSON with // credential information. - Command *exec.Cmd + command *exec.Cmd // MaxBufSize limits memory usage from growing to an enormous // amount due to a faulty process. @@ -198,10 +185,10 @@ type ProcessProvider struct { // ProcessProvider. The credentials will expire every 15 minutes by default. func NewCredentials(command string, options ...func(*ProcessProvider)) *credentials.Credentials { p := &ProcessProvider{ - Command: exec.Command(command), + command: exec.Command(command), Duration: DefaultDuration, Timeout: DefaultTimeout, - MaxBufSize: DefaultMaxBufSize, + MaxBufSize: DefaultBufSize, } for _, option := range options { @@ -221,6 +208,23 @@ func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.C return p } +// NewCredentialsCommand returns a pointer to a new Credentials object with +// the specified command, and default timeout, duration and max buffer size. +func NewCredentialsCommand(command *exec.Cmd, options ...func(*ProcessProvider)) *credentials.Credentials { + p := &ProcessProvider{ + command: command, + Duration: DefaultDuration, + Timeout: DefaultTimeout, + MaxBufSize: DefaultBufSize, + } + + for _, option := range options { + option(p) + } + + return credentials.NewCredentials(p) +} + type credentialProcessResponse struct { Version int AccessKeyID string `json:"AccessKeyId"` @@ -241,28 +245,28 @@ func (p *ProcessProvider) Retrieve() (credentials.Value, error) { if err = json.Unmarshal(out, resp); err != nil { return credentials.Value{ProviderName: ProviderName}, awserr.New( ErrCodeProcessProviderParse, - fmt.Sprintf("%s: %s", ErrMsgProcessProviderParse, string(out)), + fmt.Sprintf("%s: %s", errMsgProcessProviderParse, string(out)), err) } if resp.Version != 1 { return credentials.Value{ProviderName: ProviderName}, awserr.New( ErrCodeProcessProviderVersion, - ErrMsgProcessProviderVersion, + errMsgProcessProviderVersion, nil) } if len(resp.AccessKeyID) == 0 { return credentials.Value{ProviderName: ProviderName}, awserr.New( ErrCodeProcessProviderRequired, - ErrMsgProcessProviderMissKey, + errMsgProcessProviderMissKey, nil) } if len(resp.SecretAccessKey) == 0 { return credentials.Value{ProviderName: ProviderName}, awserr.New( ErrCodeProcessProviderRequired, - ErrMsgProcessProviderMissSecret, + errMsgProcessProviderMissSecret, nil) } @@ -293,15 +297,15 @@ func (p *ProcessProvider) IsExpired() bool { func (p *ProcessProvider) prepareCommand() error { var cmdArgs []string - if runtime.GOOS == GoOSWindowsKey { + if runtime.GOOS == "windows" { cmdArgs = []string{"cmd.exe", "/C"} } else { cmdArgs = []string{"sh", "-c"} } if len(p.originalCommand) == 0 { - p.originalCommand = make([]string, len(p.Command.Args)) - copy(p.originalCommand, p.Command.Args) + p.originalCommand = make([]string, len(p.command.Args)) + copy(p.originalCommand, p.command.Args) // check for empty command because it succeeds if len(strings.TrimSpace(p.originalCommand[0])) < 1 { @@ -309,15 +313,15 @@ func (p *ProcessProvider) prepareCommand() error { ErrCodeProcessProviderExecution, fmt.Sprintf( "%s: %s", - ErrMsgProcessProviderPrepareCmd, - ErrMsgProcessProviderEmptyCmd), + errMsgProcessProviderPrepareCmd, + errMsgProcessProviderEmptyCmd), nil) } } cmdArgs = append(cmdArgs, p.originalCommand...) - p.Command = exec.Command(cmdArgs[0], cmdArgs[1:]...) - p.Command.Env = os.Environ() + p.command = exec.Command(cmdArgs[0], cmdArgs[1:]...) + p.command.Env = os.Environ() return nil } @@ -335,13 +339,13 @@ func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) { if err != nil { return nil, awserr.New( ErrCodeProcessProviderExecution, - ErrMsgProcessProviderPipe, + errMsgProcessProviderPipe, err) } - p.Command.Stderr = os.Stderr // display stderr on console for MFA - p.Command.Stdout = outWritePipe // get creds json on process's stdout - p.Command.Stdin = os.Stdin // enable stdin for MFA + p.command.Stderr = os.Stderr // display stderr on console for MFA + p.command.Stdout = outWritePipe // get creds json on process's stdout + p.command.Stdin = os.Stdin // enable stdin for MFA output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize)) @@ -352,7 +356,7 @@ func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) { stdoutCh) execCh := make(chan error, 1) - go executeCommand(*p.Command, execCh) + go executeCommand(*p.command, execCh) finished := false var errors []error @@ -368,21 +372,21 @@ func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) { if errors != nil { return output.Bytes(), awserr.NewBatchError( ErrCodeProcessProviderExecution, - ErrMsgProcessProviderProcess, + errMsgProcessProviderProcess, errors) } case <-time.After(p.Timeout): finished = true return output.Bytes(), awserr.NewBatchError( ErrCodeProcessProviderExecution, - ErrMsgProcessProviderTimeout, + errMsgProcessProviderTimeout, errors) // errors can be nil } } out := output.Bytes() - if runtime.GOOS == GoOSWindowsKey { + if runtime.GOOS == "windows" { // windows adds slashes to quotes out = []byte(strings.Replace(string(out), `\"`, `"`, -1)) } From de02e465662d99706423b49c2cc34e6d555c0849 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Mon, 26 Nov 2018 23:59:44 -0500 Subject: [PATCH 5/5] Fix credential process tests based on review --- aws/credentials/processcreds/provider_test.go | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/aws/credentials/processcreds/provider_test.go b/aws/credentials/processcreds/provider_test.go index 22ae7d72fc3..fd3253d1cfe 100644 --- a/aws/credentials/processcreds/provider_test.go +++ b/aws/credentials/processcreds/provider_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "runtime" "strings" "testing" @@ -305,13 +306,13 @@ func TestProcessProviderTimeout(t *testing.T) { command := "/bin/sleep 2" if runtime.GOOS == "windows" { // "timeout" command does not work due to pipe redirection - command = "C:\\Windows\\system32\\ping -n 2 127.0.0.1>nul" + command = "ping -n 2 127.0.0.1>nul" } creds := processcreds.NewCredentialsTimeout( command, time.Duration(1)*time.Second) - if _, err := creds.Get(); err == nil || err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderExecution || err.(awserr.Error).Message() != processcreds.ErrMsgProcessProviderTimeout { + if _, err := creds.Get(); err == nil || err.(awserr.Error).Code() != processcreds.ErrCodeProcessProviderExecution || err.(awserr.Error).Message() != "credential process timed out" { t.Errorf("expected %v, got %v", processcreds.ErrCodeProcessProviderExecution, err) } @@ -474,6 +475,30 @@ func TestProcessProviderForceExpire(t *testing.T) { } +func TestProcessProviderAltConstruct(t *testing.T) { + oldEnv := preserveImportantStashEnv() + defer awstesting.PopEnv(oldEnv) + + // constructing with exec.Cmd instead of string + myCommand := exec.Command( + fmt.Sprintf( + "%s %s", + getOSCat(), + strings.Join( + []string{"testdata", "static.json"}, + string(os.PathSeparator)))) + creds := processcreds.NewCredentialsCommand(myCommand, func(opt *processcreds.ProcessProvider) { + opt.Timeout = time.Duration(1) * time.Second + }) + _, err := creds.Get() + if err != nil { + t.Errorf("expected %v, got %v", "no error", err) + } + if creds.IsExpired() { + t.Errorf("expected %v, got %v", "static credentials/not expired", "expired") + } +} + func BenchmarkProcessProvider(b *testing.B) { oldEnv := preserveImportantStashEnv() defer awstesting.PopEnv(oldEnv) @@ -500,18 +525,14 @@ func BenchmarkProcessProvider(b *testing.B) { } func preserveImportantStashEnv() []string { - extraEnv := make(map[string]string) + envsToKeep := []string{"PATH"} + if runtime.GOOS == "windows" { - key := "ComSpec" - if val, ok := os.LookupEnv(key); ok && len(val) > 0 { - extraEnv[key] = val - } + envsToKeep = append(envsToKeep, "ComSpec") + envsToKeep = append(envsToKeep, "SYSTEM32") } - key := "PATH" - if val, ok := os.LookupEnv(key); ok && len(val) > 0 { - extraEnv[key] = val - } + extraEnv := getEnvs(envsToKeep) oldEnv := awstesting.StashEnv() //clear env @@ -522,6 +543,16 @@ func preserveImportantStashEnv() []string { return oldEnv } +func getEnvs(envs []string) map[string]string { + extraEnvs := make(map[string]string) + for _, env := range envs { + if val, ok := os.LookupEnv(env); ok && len(val) > 0 { + extraEnvs[env] = val + } + } + return extraEnvs +} + func getOSCat() string { if runtime.GOOS == "windows" { return "type"