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	"github.com/aws/aws-sdk-go/internal/sdkio"
94)
95
96const (
97	// ProviderName is the name this credentials provider will label any
98	// returned credentials Value with.
99	ProviderName = `ProcessProvider`
100
101	// ErrCodeProcessProviderParse error parsing process output
102	ErrCodeProcessProviderParse = "ProcessProviderParseError"
103
104	// ErrCodeProcessProviderVersion version error in output
105	ErrCodeProcessProviderVersion = "ProcessProviderVersionError"
106
107	// ErrCodeProcessProviderRequired required attribute missing in output
108	ErrCodeProcessProviderRequired = "ProcessProviderRequiredError"
109
110	// ErrCodeProcessProviderExecution execution of command failed
111	ErrCodeProcessProviderExecution = "ProcessProviderExecutionError"
112
113	// errMsgProcessProviderTimeout process took longer than allowed
114	errMsgProcessProviderTimeout = "credential process timed out"
115
116	// errMsgProcessProviderProcess process error
117	errMsgProcessProviderProcess = "error in credential_process"
118
119	// errMsgProcessProviderParse problem parsing output
120	errMsgProcessProviderParse = "parse failed of credential_process output"
121
122	// errMsgProcessProviderVersion version error in output
123	errMsgProcessProviderVersion = "wrong version in process output (not 1)"
124
125	// errMsgProcessProviderMissKey missing access key id in output
126	errMsgProcessProviderMissKey = "missing AccessKeyId in process output"
127
128	// errMsgProcessProviderMissSecret missing secret acess key in output
129	errMsgProcessProviderMissSecret = "missing SecretAccessKey in process output"
130
131	// errMsgProcessProviderPrepareCmd prepare of command failed
132	errMsgProcessProviderPrepareCmd = "failed to prepare command"
133
134	// errMsgProcessProviderEmptyCmd command must not be empty
135	errMsgProcessProviderEmptyCmd = "command must not be empty"
136
137	// errMsgProcessProviderPipe failed to initialize pipe
138	errMsgProcessProviderPipe = "failed to initialize pipe"
139
140	// DefaultDuration is the default amount of time in minutes that the
141	// credentials will be valid for.
142	DefaultDuration = time.Duration(15) * time.Minute
143
144	// DefaultBufSize limits buffer size from growing to an enormous
145	// amount due to a faulty process.
146	DefaultBufSize = int(8 * sdkio.KibiByte)
147
148	// DefaultTimeout default limit on time a process can run.
149	DefaultTimeout = time.Duration(1) * time.Minute
150)
151
152// ProcessProvider satisfies the credentials.Provider interface, and is a
153// client to retrieve credentials from a process.
154type ProcessProvider struct {
155	staticCreds bool
156	credentials.Expiry
157	originalCommand []string
158
159	// Expiry duration of the credentials. Defaults to 15 minutes if not set.
160	Duration time.Duration
161
162	// ExpiryWindow will allow the credentials to trigger refreshing prior to
163	// the credentials actually expiring. This is beneficial so race conditions
164	// with expiring credentials do not cause request to fail unexpectedly
165	// due to ExpiredTokenException exceptions.
166	//
167	// So a ExpiryWindow of 10s would cause calls to IsExpired() to return true
168	// 10 seconds before the credentials are actually expired.
169	//
170	// If ExpiryWindow is 0 or less it will be ignored.
171	ExpiryWindow time.Duration
172
173	// A string representing an os command that should return a JSON with
174	// credential information.
175	command *exec.Cmd
176
177	// MaxBufSize limits memory usage from growing to an enormous
178	// amount due to a faulty process.
179	MaxBufSize int
180
181	// Timeout limits the time a process can run.
182	Timeout time.Duration
183}
184
185// NewCredentials returns a pointer to a new Credentials object wrapping the
186// ProcessProvider. The credentials will expire every 15 minutes by default.
187func NewCredentials(command string, options ...func(*ProcessProvider)) *credentials.Credentials {
188	p := &ProcessProvider{
189		command:    exec.Command(command),
190		Duration:   DefaultDuration,
191		Timeout:    DefaultTimeout,
192		MaxBufSize: DefaultBufSize,
193	}
194
195	for _, option := range options {
196		option(p)
197	}
198
199	return credentials.NewCredentials(p)
200}
201
202// NewCredentialsTimeout returns a pointer to a new Credentials object with
203// the specified command and timeout, and default duration and max buffer size.
204func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.Credentials {
205	p := NewCredentials(command, func(opt *ProcessProvider) {
206		opt.Timeout = timeout
207	})
208
209	return p
210}
211
212// NewCredentialsCommand returns a pointer to a new Credentials object with
213// the specified command, and default timeout, duration and max buffer size.
214func NewCredentialsCommand(command *exec.Cmd, options ...func(*ProcessProvider)) *credentials.Credentials {
215	p := &ProcessProvider{
216		command:    command,
217		Duration:   DefaultDuration,
218		Timeout:    DefaultTimeout,
219		MaxBufSize: DefaultBufSize,
220	}
221
222	for _, option := range options {
223		option(p)
224	}
225
226	return credentials.NewCredentials(p)
227}
228
229type credentialProcessResponse struct {
230	Version         int
231	AccessKeyID     string `json:"AccessKeyId"`
232	SecretAccessKey string
233	SessionToken    string
234	Expiration      *time.Time
235}
236
237// Retrieve executes the 'credential_process' and returns the credentials.
238func (p *ProcessProvider) Retrieve() (credentials.Value, error) {
239	out, err := p.executeCredentialProcess()
240	if err != nil {
241		return credentials.Value{ProviderName: ProviderName}, err
242	}
243
244	// Serialize and validate response
245	resp := &credentialProcessResponse{}
246	if err = json.Unmarshal(out, resp); err != nil {
247		return credentials.Value{ProviderName: ProviderName}, awserr.New(
248			ErrCodeProcessProviderParse,
249			fmt.Sprintf("%s: %s", errMsgProcessProviderParse, string(out)),
250			err)
251	}
252
253	if resp.Version != 1 {
254		return credentials.Value{ProviderName: ProviderName}, awserr.New(
255			ErrCodeProcessProviderVersion,
256			errMsgProcessProviderVersion,
257			nil)
258	}
259
260	if len(resp.AccessKeyID) == 0 {
261		return credentials.Value{ProviderName: ProviderName}, awserr.New(
262			ErrCodeProcessProviderRequired,
263			errMsgProcessProviderMissKey,
264			nil)
265	}
266
267	if len(resp.SecretAccessKey) == 0 {
268		return credentials.Value{ProviderName: ProviderName}, awserr.New(
269			ErrCodeProcessProviderRequired,
270			errMsgProcessProviderMissSecret,
271			nil)
272	}
273
274	// Handle expiration
275	p.staticCreds = resp.Expiration == nil
276	if resp.Expiration != nil {
277		p.SetExpiration(*resp.Expiration, p.ExpiryWindow)
278	}
279
280	return credentials.Value{
281		ProviderName:    ProviderName,
282		AccessKeyID:     resp.AccessKeyID,
283		SecretAccessKey: resp.SecretAccessKey,
284		SessionToken:    resp.SessionToken,
285	}, nil
286}
287
288// IsExpired returns true if the credentials retrieved are expired, or not yet
289// retrieved.
290func (p *ProcessProvider) IsExpired() bool {
291	if p.staticCreds {
292		return false
293	}
294	return p.Expiry.IsExpired()
295}
296
297// prepareCommand prepares the command to be executed.
298func (p *ProcessProvider) prepareCommand() error {
299
300	var cmdArgs []string
301	if runtime.GOOS == "windows" {
302		cmdArgs = []string{"cmd.exe", "/C"}
303	} else {
304		cmdArgs = []string{"sh", "-c"}
305	}
306
307	if len(p.originalCommand) == 0 {
308		p.originalCommand = make([]string, len(p.command.Args))
309		copy(p.originalCommand, p.command.Args)
310
311		// check for empty command because it succeeds
312		if len(strings.TrimSpace(p.originalCommand[0])) < 1 {
313			return awserr.New(
314				ErrCodeProcessProviderExecution,
315				fmt.Sprintf(
316					"%s: %s",
317					errMsgProcessProviderPrepareCmd,
318					errMsgProcessProviderEmptyCmd),
319				nil)
320		}
321	}
322
323	cmdArgs = append(cmdArgs, p.originalCommand...)
324	p.command = exec.Command(cmdArgs[0], cmdArgs[1:]...)
325	p.command.Env = os.Environ()
326
327	return nil
328}
329
330// executeCredentialProcess starts the credential process on the OS and
331// returns the results or an error.
332func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) {
333
334	if err := p.prepareCommand(); err != nil {
335		return nil, err
336	}
337
338	// Setup the pipes
339	outReadPipe, outWritePipe, err := os.Pipe()
340	if err != nil {
341		return nil, awserr.New(
342			ErrCodeProcessProviderExecution,
343			errMsgProcessProviderPipe,
344			err)
345	}
346
347	p.command.Stderr = os.Stderr    // display stderr on console for MFA
348	p.command.Stdout = outWritePipe // get creds json on process's stdout
349	p.command.Stdin = os.Stdin      // enable stdin for MFA
350
351	output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize))
352
353	stdoutCh := make(chan error, 1)
354	go readInput(
355		io.LimitReader(outReadPipe, int64(p.MaxBufSize)),
356		output,
357		stdoutCh)
358
359	execCh := make(chan error, 1)
360	go executeCommand(*p.command, execCh)
361
362	finished := false
363	var errors []error
364	for !finished {
365		select {
366		case readError := <-stdoutCh:
367			errors = appendError(errors, readError)
368			finished = true
369		case execError := <-execCh:
370			err := outWritePipe.Close()
371			errors = appendError(errors, err)
372			errors = appendError(errors, execError)
373			if errors != nil {
374				return output.Bytes(), awserr.NewBatchError(
375					ErrCodeProcessProviderExecution,
376					errMsgProcessProviderProcess,
377					errors)
378			}
379		case <-time.After(p.Timeout):
380			finished = true
381			return output.Bytes(), awserr.NewBatchError(
382				ErrCodeProcessProviderExecution,
383				errMsgProcessProviderTimeout,
384				errors) // errors can be nil
385		}
386	}
387
388	out := output.Bytes()
389
390	if runtime.GOOS == "windows" {
391		// windows adds slashes to quotes
392		out = []byte(strings.Replace(string(out), `\"`, `"`, -1))
393	}
394
395	return out, nil
396}
397
398// appendError conveniently checks for nil before appending slice
399func appendError(errors []error, err error) []error {
400	if err != nil {
401		return append(errors, err)
402	}
403	return errors
404}
405
406func executeCommand(cmd exec.Cmd, exec chan error) {
407	// Start the command
408	err := cmd.Start()
409	if err == nil {
410		err = cmd.Wait()
411	}
412
413	exec <- err
414}
415
416func readInput(r io.Reader, w io.Writer, read chan error) {
417	tee := io.TeeReader(r, w)
418
419	_, err := ioutil.ReadAll(tee)
420
421	if err == io.EOF {
422		err = nil
423	}
424
425	read <- err // will only arrive here when write end of pipe is closed
426}
427