1/*
2Package processcreds is a credential Provider to retrieve `credential_process`
3credentials.
4
5WARNING: The following describes a method of sourcing credentials from an external
6process. This can potentially be dangerous, so proceed with caution. Other
7credential providers should be preferred if at all possible. If using this
8option, you should make sure that the config file is as locked down as possible
9using security best practices for your operating system.
10
11You can use credentials from a `credential_process` in a variety of ways.
12
13One way is to setup your shared config file, located in the default
14location, with the `credential_process` key and the command you want to be
15called. You also need to set the AWS_SDK_LOAD_CONFIG environment variable
16(e.g., `export AWS_SDK_LOAD_CONFIG=1`) to use the shared config file.
17
18    [default]
19    credential_process = /command/to/call
20
21Creating a new session will use the credential process to retrieve credentials.
22NOTE: If there are credentials in the profile you are using, the credential
23process will not be used.
24
25    // Initialize a session to load credentials.
26    sess, _ := session.NewSession(&aws.Config{
27        Region: aws.String("us-east-1")},
28    )
29
30    // Create S3 service client to use the credentials.
31    svc := s3.New(sess)
32
33Another way to use the `credential_process` method is by using
34`credentials.NewCredentials()` and providing a command to be executed to
35retrieve credentials:
36
37    // Create credentials using the ProcessProvider.
38    creds := processcreds.NewCredentials("/path/to/command")
39
40    // Create service client value configured for credentials.
41    svc := s3.New(sess, &aws.Config{Credentials: creds})
42
43You can set a non-default timeout for the `credential_process` with another
44constructor, `credentials.NewCredentialsTimeout()`, providing the timeout. To
45set a one minute timeout:
46
47    // Create credentials using the ProcessProvider.
48    creds := processcreds.NewCredentialsTimeout(
49        "/path/to/command",
50        time.Duration(500) * time.Millisecond)
51
52If you need more control, you can set any configurable options in the
53credentials using one or more option functions. For example, you can set a two
54minute timeout, a credential duration of 60 minutes, and a maximum stdout
55buffer size of 2k.
56
57    creds := processcreds.NewCredentials(
58        "/path/to/command",
59        func(opt *ProcessProvider) {
60            opt.Timeout = time.Duration(2) * time.Minute
61            opt.Duration = time.Duration(60) * time.Minute
62            opt.MaxBufSize = 2048
63        })
64
65You can also use your own `exec.Cmd`:
66
67	// Create an exec.Cmd
68	myCommand := exec.Command("/path/to/command")
69
70	// Create credentials using your exec.Cmd and custom timeout
71	creds := processcreds.NewCredentialsCommand(
72		myCommand,
73		func(opt *processcreds.ProcessProvider) {
74			opt.Timeout = time.Duration(1) * time.Second
75		})
76*/
77package processcreds
78
79import (
80	"bytes"
81	"encoding/json"
82	"fmt"
83	"io"
84	"io/ioutil"
85	"os"
86	"os/exec"
87	"runtime"
88	"strings"
89	"time"
90
91	"github.com/aws/aws-sdk-go/aws/awserr"
92	"github.com/aws/aws-sdk-go/aws/credentials"
93)
94
95const (
96	// ProviderName is the name this credentials provider will label any
97	// returned credentials Value with.
98	ProviderName = `ProcessProvider`
99
100	// ErrCodeProcessProviderParse error parsing process output
101	ErrCodeProcessProviderParse = "ProcessProviderParseError"
102
103	// ErrCodeProcessProviderVersion version error in output
104	ErrCodeProcessProviderVersion = "ProcessProviderVersionError"
105
106	// ErrCodeProcessProviderRequired required attribute missing in output
107	ErrCodeProcessProviderRequired = "ProcessProviderRequiredError"
108
109	// ErrCodeProcessProviderExecution execution of command failed
110	ErrCodeProcessProviderExecution = "ProcessProviderExecutionError"
111
112	// errMsgProcessProviderTimeout process took longer than allowed
113	errMsgProcessProviderTimeout = "credential process timed out"
114
115	// errMsgProcessProviderProcess process error
116	errMsgProcessProviderProcess = "error in credential_process"
117
118	// errMsgProcessProviderParse problem parsing output
119	errMsgProcessProviderParse = "parse failed of credential_process output"
120
121	// errMsgProcessProviderVersion version error in output
122	errMsgProcessProviderVersion = "wrong version in process output (not 1)"
123
124	// errMsgProcessProviderMissKey missing access key id in output
125	errMsgProcessProviderMissKey = "missing AccessKeyId in process output"
126
127	// errMsgProcessProviderMissSecret missing secret acess key in output
128	errMsgProcessProviderMissSecret = "missing SecretAccessKey in process output"
129
130	// errMsgProcessProviderPrepareCmd prepare of command failed
131	errMsgProcessProviderPrepareCmd = "failed to prepare command"
132
133	// errMsgProcessProviderEmptyCmd command must not be empty
134	errMsgProcessProviderEmptyCmd = "command must not be empty"
135
136	// errMsgProcessProviderPipe failed to initialize pipe
137	errMsgProcessProviderPipe = "failed to initialize pipe"
138
139	// DefaultDuration is the default amount of time in minutes that the
140	// credentials will be valid for.
141	DefaultDuration = time.Duration(15) * time.Minute
142
143	// DefaultBufSize limits buffer size from growing to an enormous
144	// amount due to a faulty process.
145	DefaultBufSize = 1024
146
147	// DefaultTimeout default limit on time a process can run.
148	DefaultTimeout = time.Duration(1) * time.Minute
149)
150
151// ProcessProvider satisfies the credentials.Provider interface, and is a
152// client to retrieve credentials from a process.
153type ProcessProvider struct {
154	staticCreds bool
155	credentials.Expiry
156	originalCommand []string
157
158	// Expiry duration of the credentials. Defaults to 15 minutes if not set.
159	Duration time.Duration
160
161	// ExpiryWindow will allow the credentials to trigger refreshing prior to
162	// the credentials actually expiring. This is beneficial so race conditions
163	// with expiring credentials do not cause request to fail unexpectedly
164	// due to ExpiredTokenException exceptions.
165	//
166	// So a ExpiryWindow of 10s would cause calls to IsExpired() to return true
167	// 10 seconds before the credentials are actually expired.
168	//
169	// If ExpiryWindow is 0 or less it will be ignored.
170	ExpiryWindow time.Duration
171
172	// A string representing an os command that should return a JSON with
173	// credential information.
174	command *exec.Cmd
175
176	// MaxBufSize limits memory usage from growing to an enormous
177	// amount due to a faulty process.
178	MaxBufSize int
179
180	// Timeout limits the time a process can run.
181	Timeout time.Duration
182}
183
184// NewCredentials returns a pointer to a new Credentials object wrapping the
185// ProcessProvider. The credentials will expire every 15 minutes by default.
186func NewCredentials(command string, options ...func(*ProcessProvider)) *credentials.Credentials {
187	p := &ProcessProvider{
188		command:    exec.Command(command),
189		Duration:   DefaultDuration,
190		Timeout:    DefaultTimeout,
191		MaxBufSize: DefaultBufSize,
192	}
193
194	for _, option := range options {
195		option(p)
196	}
197
198	return credentials.NewCredentials(p)
199}
200
201// NewCredentialsTimeout returns a pointer to a new Credentials object with
202// the specified command and timeout, and default duration and max buffer size.
203func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.Credentials {
204	p := NewCredentials(command, func(opt *ProcessProvider) {
205		opt.Timeout = timeout
206	})
207
208	return p
209}
210
211// NewCredentialsCommand returns a pointer to a new Credentials object with
212// the specified command, and default timeout, duration and max buffer size.
213func NewCredentialsCommand(command *exec.Cmd, options ...func(*ProcessProvider)) *credentials.Credentials {
214	p := &ProcessProvider{
215		command:    command,
216		Duration:   DefaultDuration,
217		Timeout:    DefaultTimeout,
218		MaxBufSize: DefaultBufSize,
219	}
220
221	for _, option := range options {
222		option(p)
223	}
224
225	return credentials.NewCredentials(p)
226}
227
228type credentialProcessResponse struct {
229	Version         int
230	AccessKeyID     string `json:"AccessKeyId"`
231	SecretAccessKey string
232	SessionToken    string
233	Expiration      *time.Time
234}
235
236// Retrieve executes the 'credential_process' and returns the credentials.
237func (p *ProcessProvider) Retrieve() (credentials.Value, error) {
238	out, err := p.executeCredentialProcess()
239	if err != nil {
240		return credentials.Value{ProviderName: ProviderName}, err
241	}
242
243	// Serialize and validate response
244	resp := &credentialProcessResponse{}
245	if err = json.Unmarshal(out, resp); err != nil {
246		return credentials.Value{ProviderName: ProviderName}, awserr.New(
247			ErrCodeProcessProviderParse,
248			fmt.Sprintf("%s: %s", errMsgProcessProviderParse, string(out)),
249			err)
250	}
251
252	if resp.Version != 1 {
253		return credentials.Value{ProviderName: ProviderName}, awserr.New(
254			ErrCodeProcessProviderVersion,
255			errMsgProcessProviderVersion,
256			nil)
257	}
258
259	if len(resp.AccessKeyID) == 0 {
260		return credentials.Value{ProviderName: ProviderName}, awserr.New(
261			ErrCodeProcessProviderRequired,
262			errMsgProcessProviderMissKey,
263			nil)
264	}
265
266	if len(resp.SecretAccessKey) == 0 {
267		return credentials.Value{ProviderName: ProviderName}, awserr.New(
268			ErrCodeProcessProviderRequired,
269			errMsgProcessProviderMissSecret,
270			nil)
271	}
272
273	// Handle expiration
274	p.staticCreds = resp.Expiration == nil
275	if resp.Expiration != nil {
276		p.SetExpiration(*resp.Expiration, p.ExpiryWindow)
277	}
278
279	return credentials.Value{
280		ProviderName:    ProviderName,
281		AccessKeyID:     resp.AccessKeyID,
282		SecretAccessKey: resp.SecretAccessKey,
283		SessionToken:    resp.SessionToken,
284	}, nil
285}
286
287// IsExpired returns true if the credentials retrieved are expired, or not yet
288// retrieved.
289func (p *ProcessProvider) IsExpired() bool {
290	if p.staticCreds {
291		return false
292	}
293	return p.Expiry.IsExpired()
294}
295
296// prepareCommand prepares the command to be executed.
297func (p *ProcessProvider) prepareCommand() error {
298
299	var cmdArgs []string
300	if runtime.GOOS == "windows" {
301		cmdArgs = []string{"cmd.exe", "/C"}
302	} else {
303		cmdArgs = []string{"sh", "-c"}
304	}
305
306	if len(p.originalCommand) == 0 {
307		p.originalCommand = make([]string, len(p.command.Args))
308		copy(p.originalCommand, p.command.Args)
309
310		// check for empty command because it succeeds
311		if len(strings.TrimSpace(p.originalCommand[0])) < 1 {
312			return awserr.New(
313				ErrCodeProcessProviderExecution,
314				fmt.Sprintf(
315					"%s: %s",
316					errMsgProcessProviderPrepareCmd,
317					errMsgProcessProviderEmptyCmd),
318				nil)
319		}
320	}
321
322	cmdArgs = append(cmdArgs, p.originalCommand...)
323	p.command = exec.Command(cmdArgs[0], cmdArgs[1:]...)
324	p.command.Env = os.Environ()
325
326	return nil
327}
328
329// executeCredentialProcess starts the credential process on the OS and
330// returns the results or an error.
331func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) {
332
333	if err := p.prepareCommand(); err != nil {
334		return nil, err
335	}
336
337	// Setup the pipes
338	outReadPipe, outWritePipe, err := os.Pipe()
339	if err != nil {
340		return nil, awserr.New(
341			ErrCodeProcessProviderExecution,
342			errMsgProcessProviderPipe,
343			err)
344	}
345
346	p.command.Stderr = os.Stderr    // display stderr on console for MFA
347	p.command.Stdout = outWritePipe // get creds json on process's stdout
348	p.command.Stdin = os.Stdin      // enable stdin for MFA
349
350	output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize))
351
352	stdoutCh := make(chan error, 1)
353	go readInput(
354		io.LimitReader(outReadPipe, int64(p.MaxBufSize)),
355		output,
356		stdoutCh)
357
358	execCh := make(chan error, 1)
359	go executeCommand(*p.command, execCh)
360
361	finished := false
362	var errors []error
363	for !finished {
364		select {
365		case readError := <-stdoutCh:
366			errors = appendError(errors, readError)
367			finished = true
368		case execError := <-execCh:
369			err := outWritePipe.Close()
370			errors = appendError(errors, err)
371			errors = appendError(errors, execError)
372			if errors != nil {
373				return output.Bytes(), awserr.NewBatchError(
374					ErrCodeProcessProviderExecution,
375					errMsgProcessProviderProcess,
376					errors)
377			}
378		case <-time.After(p.Timeout):
379			finished = true
380			return output.Bytes(), awserr.NewBatchError(
381				ErrCodeProcessProviderExecution,
382				errMsgProcessProviderTimeout,
383				errors) // errors can be nil
384		}
385	}
386
387	out := output.Bytes()
388
389	if runtime.GOOS == "windows" {
390		// windows adds slashes to quotes
391		out = []byte(strings.Replace(string(out), `\"`, `"`, -1))
392	}
393
394	return out, nil
395}
396
397// appendError conveniently checks for nil before appending slice
398func appendError(errors []error, err error) []error {
399	if err != nil {
400		return append(errors, err)
401	}
402	return errors
403}
404
405func executeCommand(cmd exec.Cmd, exec chan error) {
406	// Start the command
407	err := cmd.Start()
408	if err == nil {
409		err = cmd.Wait()
410	}
411
412	exec <- err
413}
414
415func readInput(r io.Reader, w io.Writer, read chan error) {
416	tee := io.TeeReader(r, w)
417
418	_, err := ioutil.ReadAll(tee)
419
420	if err == io.EOF {
421		err = nil
422	}
423
424	read <- err // will only arrive here when write end of pipe is closed
425}
426