github.com/aavshr/aws-sdk-go@v1.41.3/aws/credentials/processcreds/provider.go (about)

     1  /*
     2  Package processcreds is a credential Provider to retrieve `credential_process`
     3  credentials.
     4  
     5  WARNING: The following describes a method of sourcing credentials from an external
     6  process. This can potentially be dangerous, so proceed with caution. Other
     7  credential providers should be preferred if at all possible. If using this
     8  option, you should make sure that the config file is as locked down as possible
     9  using security best practices for your operating system.
    10  
    11  You can use credentials from a `credential_process` in a variety of ways.
    12  
    13  One way is to setup your shared config file, located in the default
    14  location, with the `credential_process` key and the command you want to be
    15  called. 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  
    21  Creating a new session will use the credential process to retrieve credentials.
    22  NOTE: If there are credentials in the profile you are using, the credential
    23  process 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  
    33  Another way to use the `credential_process` method is by using
    34  `credentials.NewCredentials()` and providing a command to be executed to
    35  retrieve 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  
    43  You can set a non-default timeout for the `credential_process` with another
    44  constructor, `credentials.NewCredentialsTimeout()`, providing the timeout. To
    45  set 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  
    52  If you need more control, you can set any configurable options in the
    53  credentials using one or more option functions. For example, you can set a two
    54  minute timeout, a credential duration of 60 minutes, and a maximum stdout
    55  buffer 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  
    65  You 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  */
    77  package processcreds
    78  
    79  import (
    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/aavshr/aws-sdk-go/aws/awserr"
    92  	"github.com/aavshr/aws-sdk-go/aws/credentials"
    93  	"github.com/aavshr/aws-sdk-go/internal/sdkio"
    94  )
    95  
    96  const (
    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.
   154  type 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.
   187  func 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.
   204  func 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.
   214  func 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  
   229  type 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.
   238  func (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.
   290  func (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.
   298  func (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.
   332  func (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
   399  func appendError(errors []error, err error) []error {
   400  	if err != nil {
   401  		return append(errors, err)
   402  	}
   403  	return errors
   404  }
   405  
   406  func 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  
   416  func 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  }