...

Source file src/github.com/aws/aws-sdk-go-v2/credentials/processcreds/provider.go

Documentation: github.com/aws/aws-sdk-go-v2/credentials/processcreds

     1  package processcreds
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"runtime"
    12  	"time"
    13  
    14  	"github.com/aws/aws-sdk-go-v2/aws"
    15  	"github.com/aws/aws-sdk-go-v2/internal/sdkio"
    16  )
    17  
    18  const (
    19  	// ProviderName is the name this credentials provider will label any
    20  	// returned credentials Value with.
    21  	ProviderName = `ProcessProvider`
    22  
    23  	// DefaultTimeout default limit on time a process can run.
    24  	DefaultTimeout = time.Duration(1) * time.Minute
    25  )
    26  
    27  // ProviderError is an error indicating failure initializing or executing the
    28  // process credentials provider
    29  type ProviderError struct {
    30  	Err error
    31  }
    32  
    33  // Error returns the error message.
    34  func (e *ProviderError) Error() string {
    35  	return fmt.Sprintf("process provider error: %v", e.Err)
    36  }
    37  
    38  // Unwrap returns the underlying error the provider error wraps.
    39  func (e *ProviderError) Unwrap() error {
    40  	return e.Err
    41  }
    42  
    43  // Provider satisfies the credentials.Provider interface, and is a
    44  // client to retrieve credentials from a process.
    45  type Provider struct {
    46  	// Provides a constructor for exec.Cmd that are invoked by the provider for
    47  	// retrieving credentials. Use this to provide custom creation of exec.Cmd
    48  	// with things like environment variables, or other configuration.
    49  	//
    50  	// The provider defaults to the DefaultNewCommand function.
    51  	commandBuilder NewCommandBuilder
    52  
    53  	options Options
    54  }
    55  
    56  // Options is the configuration options for configuring the Provider.
    57  type Options struct {
    58  	// Timeout limits the time a process can run.
    59  	Timeout time.Duration
    60  }
    61  
    62  // NewCommandBuilder provides the interface for specifying how command will be
    63  // created that the Provider will use to retrieve credentials with.
    64  type NewCommandBuilder interface {
    65  	NewCommand(context.Context) (*exec.Cmd, error)
    66  }
    67  
    68  // NewCommandBuilderFunc provides a wrapper type around a function pointer to
    69  // satisfy the NewCommandBuilder interface.
    70  type NewCommandBuilderFunc func(context.Context) (*exec.Cmd, error)
    71  
    72  // NewCommand calls the underlying function pointer the builder was initialized with.
    73  func (fn NewCommandBuilderFunc) NewCommand(ctx context.Context) (*exec.Cmd, error) {
    74  	return fn(ctx)
    75  }
    76  
    77  // DefaultNewCommandBuilder provides the default NewCommandBuilder
    78  // implementation used by the provider. It takes a command and arguments to
    79  // invoke. The command will also be initialized with the current process
    80  // environment variables, stderr, and stdin pipes.
    81  type DefaultNewCommandBuilder struct {
    82  	Args []string
    83  }
    84  
    85  // NewCommand returns an initialized exec.Cmd with the builder's initialized
    86  // Args. The command is also initialized current process environment variables,
    87  // stderr, and stdin pipes.
    88  func (b DefaultNewCommandBuilder) NewCommand(ctx context.Context) (*exec.Cmd, error) {
    89  	var cmdArgs []string
    90  	if runtime.GOOS == "windows" {
    91  		cmdArgs = []string{"cmd.exe", "/C"}
    92  	} else {
    93  		cmdArgs = []string{"sh", "-c"}
    94  	}
    95  
    96  	if len(b.Args) == 0 {
    97  		return nil, &ProviderError{
    98  			Err: fmt.Errorf("failed to prepare command: command must not be empty"),
    99  		}
   100  	}
   101  
   102  	cmdArgs = append(cmdArgs, b.Args...)
   103  	cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...)
   104  	cmd.Env = os.Environ()
   105  
   106  	cmd.Stderr = os.Stderr // display stderr on console for MFA
   107  	cmd.Stdin = os.Stdin   // enable stdin for MFA
   108  
   109  	return cmd, nil
   110  }
   111  
   112  // NewProvider returns a pointer to a new Credentials object wrapping the
   113  // Provider.
   114  //
   115  // The provider defaults to the DefaultNewCommandBuilder for creating command
   116  // the Provider will use to retrieve credentials with.
   117  func NewProvider(command string, options ...func(*Options)) *Provider {
   118  	var args []string
   119  
   120  	// Ensure that the command arguments are not set if the provided command is
   121  	// empty. This will error out when the command is executed since no
   122  	// arguments are specified.
   123  	if len(command) > 0 {
   124  		args = []string{command}
   125  	}
   126  
   127  	commanBuilder := DefaultNewCommandBuilder{
   128  		Args: args,
   129  	}
   130  	return NewProviderCommand(commanBuilder, options...)
   131  }
   132  
   133  // NewProviderCommand returns a pointer to a new Credentials object with the
   134  // specified command, and default timeout duration. Use this to provide custom
   135  // creation of exec.Cmd for options like environment variables, or other
   136  // configuration.
   137  func NewProviderCommand(builder NewCommandBuilder, options ...func(*Options)) *Provider {
   138  	p := &Provider{
   139  		commandBuilder: builder,
   140  		options: Options{
   141  			Timeout: DefaultTimeout,
   142  		},
   143  	}
   144  
   145  	for _, option := range options {
   146  		option(&p.options)
   147  	}
   148  
   149  	return p
   150  }
   151  
   152  // A CredentialProcessResponse is the AWS credentials format that must be
   153  // returned when executing an external credential_process.
   154  type CredentialProcessResponse struct {
   155  	// As of this writing, the Version key must be set to 1. This might
   156  	// increment over time as the structure evolves.
   157  	Version int
   158  
   159  	// The access key ID that identifies the temporary security credentials.
   160  	AccessKeyID string `json:"AccessKeyId"`
   161  
   162  	// The secret access key that can be used to sign requests.
   163  	SecretAccessKey string
   164  
   165  	// The token that users must pass to the service API to use the temporary credentials.
   166  	SessionToken string
   167  
   168  	// The date on which the current credentials expire.
   169  	Expiration *time.Time
   170  }
   171  
   172  // Retrieve executes the credential process command and returns the
   173  // credentials, or error if the command fails.
   174  func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) {
   175  	out, err := p.executeCredentialProcess(ctx)
   176  	if err != nil {
   177  		return aws.Credentials{Source: ProviderName}, err
   178  	}
   179  
   180  	// Serialize and validate response
   181  	resp := &CredentialProcessResponse{}
   182  	if err = json.Unmarshal(out, resp); err != nil {
   183  		return aws.Credentials{Source: ProviderName}, &ProviderError{
   184  			Err: fmt.Errorf("parse failed of process output: %s, error: %w", out, err),
   185  		}
   186  	}
   187  
   188  	if resp.Version != 1 {
   189  		return aws.Credentials{Source: ProviderName}, &ProviderError{
   190  			Err: fmt.Errorf("wrong version in process output (not 1)"),
   191  		}
   192  	}
   193  
   194  	if len(resp.AccessKeyID) == 0 {
   195  		return aws.Credentials{Source: ProviderName}, &ProviderError{
   196  			Err: fmt.Errorf("missing AccessKeyId in process output"),
   197  		}
   198  	}
   199  
   200  	if len(resp.SecretAccessKey) == 0 {
   201  		return aws.Credentials{Source: ProviderName}, &ProviderError{
   202  			Err: fmt.Errorf("missing SecretAccessKey in process output"),
   203  		}
   204  	}
   205  
   206  	creds := aws.Credentials{
   207  		Source:          ProviderName,
   208  		AccessKeyID:     resp.AccessKeyID,
   209  		SecretAccessKey: resp.SecretAccessKey,
   210  		SessionToken:    resp.SessionToken,
   211  	}
   212  
   213  	// Handle expiration
   214  	if resp.Expiration != nil {
   215  		creds.CanExpire = true
   216  		creds.Expires = *resp.Expiration
   217  	}
   218  
   219  	return creds, nil
   220  }
   221  
   222  // executeCredentialProcess starts the credential process on the OS and
   223  // returns the results or an error.
   224  func (p *Provider) executeCredentialProcess(ctx context.Context) ([]byte, error) {
   225  	if p.options.Timeout >= 0 {
   226  		var cancelFunc func()
   227  		ctx, cancelFunc = context.WithTimeout(ctx, p.options.Timeout)
   228  		defer cancelFunc()
   229  	}
   230  
   231  	cmd, err := p.commandBuilder.NewCommand(ctx)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	// get creds json on process's stdout
   237  	output := bytes.NewBuffer(make([]byte, 0, int(8*sdkio.KibiByte)))
   238  	if cmd.Stdout != nil {
   239  		cmd.Stdout = io.MultiWriter(cmd.Stdout, output)
   240  	} else {
   241  		cmd.Stdout = output
   242  	}
   243  
   244  	execCh := make(chan error, 1)
   245  	go executeCommand(cmd, execCh)
   246  
   247  	select {
   248  	case execError := <-execCh:
   249  		if execError == nil {
   250  			break
   251  		}
   252  		select {
   253  		case <-ctx.Done():
   254  			return output.Bytes(), &ProviderError{
   255  				Err: fmt.Errorf("credential process timed out: %w", execError),
   256  			}
   257  		default:
   258  			return output.Bytes(), &ProviderError{
   259  				Err: fmt.Errorf("error in credential_process: %w", execError),
   260  			}
   261  		}
   262  	}
   263  
   264  	out := output.Bytes()
   265  	if runtime.GOOS == "windows" {
   266  		// windows adds slashes to quotes
   267  		out = bytes.ReplaceAll(out, []byte(`\"`), []byte(`"`))
   268  	}
   269  
   270  	return out, nil
   271  }
   272  
   273  func executeCommand(cmd *exec.Cmd, exec chan error) {
   274  	// Start the command
   275  	err := cmd.Start()
   276  	if err == nil {
   277  		err = cmd.Wait()
   278  	}
   279  
   280  	exec <- err
   281  }
   282  

View as plain text