-
Notifications
You must be signed in to change notification settings - Fork 2.1k
aws/credential: Added credential_process provider #1874
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,3 +9,4 @@ awstesting/integration/smoke/_test/ | |
| /vendor/pkg/ | ||
| /vendor/src/ | ||
| /private/model/cli/gen-api/gen-api | ||
| .*.swp | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| package credentials | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would you mind moving the |
||
|
|
||
| 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()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Loading the shared config file each time the credentials are retrieved have issues that I don't think is good for the SDK to perform. per #1993 |
||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to look at exporting aws/session/shard_config.go or at least put it under the SDK's root
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think ideally the ProcessProvider doesn't need to read/load the shared config file at all. The loading of the contents of the shared config's If the |
||
| if err != nil { | ||
| return Value{ProviderName: ProcessProviderName}, awserr.New("ProcessProviderLoad", "failed to load shared config file", err) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For these error messages could you please create consts to make comparing against them easier. e.g: const (
ErrCodeProcessProviderLoad = `ProcessProviderLoad`
) |
||
| } | ||
|
|
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can |
||
| } | ||
|
|
||
| func executeCredentialProcess(process string) ([]byte, error) { | ||
| processArgs := strings.Split(process, " ") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this split will have issue with quoted strings within the argument list of the will be split into: []string{"ls", "\"my", "cool/path", "with/spaces\""} |
||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is probably best to let |
||
| 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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the |
||
| out, err := cmd.Output() | ||
| if err != nil { | ||
| return nil, awserr.New("ProcessProviderLoad", "Error executing credential_process", err) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this capture |
||
| } | ||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a pointer for |
||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This information should come from the SDK's |
||
| 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") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The SDK does not use the |
||
| } | ||
| if p.Profile == "" { | ||
| p.Profile = "default" | ||
| } | ||
|
|
||
| return p.Profile | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| package credentials | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The SDK is looking to move away from |
||
| ) | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit this file probably shouldn't change