Fixes https://github.com/docker/distribution/issues/3097 Signed-off-by: Yong Wen Chua <lawliet89@users.noreply.github.com>
426 lines
12 KiB
Go
426 lines
12 KiB
Go
/*
|
|
Package processcreds is a credential Provider to retrieve `credential_process`
|
|
credentials.
|
|
|
|
WARNING: The following describes a method of sourcing credentials from an external
|
|
process. This can potentially be dangerous, so proceed with caution. Other
|
|
credential providers should be preferred if at all possible. If using this
|
|
option, you should make sure that the config file is as locked down as possible
|
|
using security best practices for your operating system.
|
|
|
|
You can use credentials from a `credential_process` in a variety of ways.
|
|
|
|
One way is to setup your shared config file, located in the default
|
|
location, with the `credential_process` key and the command you want to be
|
|
called. You also need to set the AWS_SDK_LOAD_CONFIG environment variable
|
|
(e.g., `export AWS_SDK_LOAD_CONFIG=1`) to use the shared config file.
|
|
|
|
[default]
|
|
credential_process = /command/to/call
|
|
|
|
Creating a new session will use the credential process to retrieve credentials.
|
|
NOTE: If there are credentials in the profile you are using, the credential
|
|
process will not be used.
|
|
|
|
// Initialize a session to load credentials.
|
|
sess, _ := session.NewSession(&aws.Config{
|
|
Region: aws.String("us-east-1")},
|
|
)
|
|
|
|
// Create S3 service client to use the credentials.
|
|
svc := s3.New(sess)
|
|
|
|
Another way to use the `credential_process` method is by using
|
|
`credentials.NewCredentials()` and providing a command to be executed to
|
|
retrieve credentials:
|
|
|
|
// Create credentials using the ProcessProvider.
|
|
creds := processcreds.NewCredentials("/path/to/command")
|
|
|
|
// Create service client value configured for credentials.
|
|
svc := s3.New(sess, &aws.Config{Credentials: creds})
|
|
|
|
You can set a non-default timeout for the `credential_process` with another
|
|
constructor, `credentials.NewCredentialsTimeout()`, providing the timeout. To
|
|
set a one minute timeout:
|
|
|
|
// Create credentials using the ProcessProvider.
|
|
creds := processcreds.NewCredentialsTimeout(
|
|
"/path/to/command",
|
|
time.Duration(500) * time.Millisecond)
|
|
|
|
If you need more control, you can set any configurable options in the
|
|
credentials using one or more option functions. For example, you can set a two
|
|
minute timeout, a credential duration of 60 minutes, and a maximum stdout
|
|
buffer size of 2k.
|
|
|
|
creds := processcreds.NewCredentials(
|
|
"/path/to/command",
|
|
func(opt *ProcessProvider) {
|
|
opt.Timeout = time.Duration(2) * time.Minute
|
|
opt.Duration = time.Duration(60) * time.Minute
|
|
opt.MaxBufSize = 2048
|
|
})
|
|
|
|
You can also use your own `exec.Cmd`:
|
|
|
|
// Create an exec.Cmd
|
|
myCommand := exec.Command("/path/to/command")
|
|
|
|
// Create credentials using your exec.Cmd and custom timeout
|
|
creds := processcreds.NewCredentialsCommand(
|
|
myCommand,
|
|
func(opt *processcreds.ProcessProvider) {
|
|
opt.Timeout = time.Duration(1) * time.Second
|
|
})
|
|
*/
|
|
package processcreds
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
"github.com/aws/aws-sdk-go/internal/sdkio"
|
|
)
|
|
|
|
const (
|
|
// ProviderName is the name this credentials provider will label any
|
|
// returned credentials Value with.
|
|
ProviderName = `ProcessProvider`
|
|
|
|
// ErrCodeProcessProviderParse error parsing process output
|
|
ErrCodeProcessProviderParse = "ProcessProviderParseError"
|
|
|
|
// ErrCodeProcessProviderVersion version error in output
|
|
ErrCodeProcessProviderVersion = "ProcessProviderVersionError"
|
|
|
|
// ErrCodeProcessProviderRequired required attribute missing in output
|
|
ErrCodeProcessProviderRequired = "ProcessProviderRequiredError"
|
|
|
|
// ErrCodeProcessProviderExecution execution of command failed
|
|
ErrCodeProcessProviderExecution = "ProcessProviderExecutionError"
|
|
|
|
// errMsgProcessProviderTimeout process took longer than allowed
|
|
errMsgProcessProviderTimeout = "credential process timed out"
|
|
|
|
// errMsgProcessProviderProcess process error
|
|
errMsgProcessProviderProcess = "error in credential_process"
|
|
|
|
// errMsgProcessProviderParse problem parsing output
|
|
errMsgProcessProviderParse = "parse failed of credential_process output"
|
|
|
|
// errMsgProcessProviderVersion version error in output
|
|
errMsgProcessProviderVersion = "wrong version in process output (not 1)"
|
|
|
|
// errMsgProcessProviderMissKey missing access key id in output
|
|
errMsgProcessProviderMissKey = "missing AccessKeyId in process output"
|
|
|
|
// errMsgProcessProviderMissSecret missing secret acess key in output
|
|
errMsgProcessProviderMissSecret = "missing SecretAccessKey in process output"
|
|
|
|
// errMsgProcessProviderPrepareCmd prepare of command failed
|
|
errMsgProcessProviderPrepareCmd = "failed to prepare command"
|
|
|
|
// errMsgProcessProviderEmptyCmd command must not be empty
|
|
errMsgProcessProviderEmptyCmd = "command must not be empty"
|
|
|
|
// errMsgProcessProviderPipe failed to initialize pipe
|
|
errMsgProcessProviderPipe = "failed to initialize pipe"
|
|
|
|
// DefaultDuration is the default amount of time in minutes that the
|
|
// credentials will be valid for.
|
|
DefaultDuration = time.Duration(15) * time.Minute
|
|
|
|
// DefaultBufSize limits buffer size from growing to an enormous
|
|
// amount due to a faulty process.
|
|
DefaultBufSize = int(8 * sdkio.KibiByte)
|
|
|
|
// DefaultTimeout default limit on time a process can run.
|
|
DefaultTimeout = time.Duration(1) * time.Minute
|
|
)
|
|
|
|
// ProcessProvider satisfies the credentials.Provider interface, and is a
|
|
// client to retrieve credentials from a process.
|
|
type ProcessProvider struct {
|
|
staticCreds bool
|
|
credentials.Expiry
|
|
originalCommand []string
|
|
|
|
// Expiry duration of the credentials. Defaults to 15 minutes if not set.
|
|
Duration time.Duration
|
|
|
|
// 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 string representing an os command that should return a JSON with
|
|
// credential information.
|
|
command *exec.Cmd
|
|
|
|
// MaxBufSize limits memory usage from growing to an enormous
|
|
// amount due to a faulty process.
|
|
MaxBufSize int
|
|
|
|
// Timeout limits the time a process can run.
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// NewCredentials returns a pointer to a new Credentials object wrapping the
|
|
// ProcessProvider. The credentials will expire every 15 minutes by default.
|
|
func NewCredentials(command string, options ...func(*ProcessProvider)) *credentials.Credentials {
|
|
p := &ProcessProvider{
|
|
command: exec.Command(command),
|
|
Duration: DefaultDuration,
|
|
Timeout: DefaultTimeout,
|
|
MaxBufSize: DefaultBufSize,
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(p)
|
|
}
|
|
|
|
return credentials.NewCredentials(p)
|
|
}
|
|
|
|
// NewCredentialsTimeout returns a pointer to a new Credentials object with
|
|
// the specified command and timeout, and default duration and max buffer size.
|
|
func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.Credentials {
|
|
p := NewCredentials(command, func(opt *ProcessProvider) {
|
|
opt.Timeout = timeout
|
|
})
|
|
|
|
return p
|
|
}
|
|
|
|
// NewCredentialsCommand returns a pointer to a new Credentials object with
|
|
// the specified command, and default timeout, duration and max buffer size.
|
|
func NewCredentialsCommand(command *exec.Cmd, options ...func(*ProcessProvider)) *credentials.Credentials {
|
|
p := &ProcessProvider{
|
|
command: command,
|
|
Duration: DefaultDuration,
|
|
Timeout: DefaultTimeout,
|
|
MaxBufSize: DefaultBufSize,
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(p)
|
|
}
|
|
|
|
return credentials.NewCredentials(p)
|
|
}
|
|
|
|
type credentialProcessResponse struct {
|
|
Version int
|
|
AccessKeyID string `json:"AccessKeyId"`
|
|
SecretAccessKey string
|
|
SessionToken string
|
|
Expiration *time.Time
|
|
}
|
|
|
|
// Retrieve executes the 'credential_process' and returns the credentials.
|
|
func (p *ProcessProvider) Retrieve() (credentials.Value, error) {
|
|
out, err := p.executeCredentialProcess()
|
|
if err != nil {
|
|
return credentials.Value{ProviderName: ProviderName}, err
|
|
}
|
|
|
|
// Serialize and validate response
|
|
resp := &credentialProcessResponse{}
|
|
if err = json.Unmarshal(out, resp); err != nil {
|
|
return credentials.Value{ProviderName: ProviderName}, awserr.New(
|
|
ErrCodeProcessProviderParse,
|
|
fmt.Sprintf("%s: %s", errMsgProcessProviderParse, string(out)),
|
|
err)
|
|
}
|
|
|
|
if resp.Version != 1 {
|
|
return credentials.Value{ProviderName: ProviderName}, awserr.New(
|
|
ErrCodeProcessProviderVersion,
|
|
errMsgProcessProviderVersion,
|
|
nil)
|
|
}
|
|
|
|
if len(resp.AccessKeyID) == 0 {
|
|
return credentials.Value{ProviderName: ProviderName}, awserr.New(
|
|
ErrCodeProcessProviderRequired,
|
|
errMsgProcessProviderMissKey,
|
|
nil)
|
|
}
|
|
|
|
if len(resp.SecretAccessKey) == 0 {
|
|
return credentials.Value{ProviderName: ProviderName}, awserr.New(
|
|
ErrCodeProcessProviderRequired,
|
|
errMsgProcessProviderMissSecret,
|
|
nil)
|
|
}
|
|
|
|
// Handle expiration
|
|
p.staticCreds = resp.Expiration == nil
|
|
if resp.Expiration != nil {
|
|
p.SetExpiration(*resp.Expiration, p.ExpiryWindow)
|
|
}
|
|
|
|
return credentials.Value{
|
|
ProviderName: ProviderName,
|
|
AccessKeyID: resp.AccessKeyID,
|
|
SecretAccessKey: resp.SecretAccessKey,
|
|
SessionToken: resp.SessionToken,
|
|
}, nil
|
|
}
|
|
|
|
// IsExpired returns true if the credentials retrieved are expired, or not yet
|
|
// retrieved.
|
|
func (p *ProcessProvider) IsExpired() bool {
|
|
if p.staticCreds {
|
|
return false
|
|
}
|
|
return p.Expiry.IsExpired()
|
|
}
|
|
|
|
// prepareCommand prepares the command to be executed.
|
|
func (p *ProcessProvider) prepareCommand() error {
|
|
|
|
var cmdArgs []string
|
|
if runtime.GOOS == "windows" {
|
|
cmdArgs = []string{"cmd.exe", "/C"}
|
|
} else {
|
|
cmdArgs = []string{"sh", "-c"}
|
|
}
|
|
|
|
if len(p.originalCommand) == 0 {
|
|
p.originalCommand = make([]string, len(p.command.Args))
|
|
copy(p.originalCommand, p.command.Args)
|
|
|
|
// check for empty command because it succeeds
|
|
if len(strings.TrimSpace(p.originalCommand[0])) < 1 {
|
|
return awserr.New(
|
|
ErrCodeProcessProviderExecution,
|
|
fmt.Sprintf(
|
|
"%s: %s",
|
|
errMsgProcessProviderPrepareCmd,
|
|
errMsgProcessProviderEmptyCmd),
|
|
nil)
|
|
}
|
|
}
|
|
|
|
cmdArgs = append(cmdArgs, p.originalCommand...)
|
|
p.command = exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
|
p.command.Env = os.Environ()
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeCredentialProcess starts the credential process on the OS and
|
|
// returns the results or an error.
|
|
func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) {
|
|
|
|
if err := p.prepareCommand(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup the pipes
|
|
outReadPipe, outWritePipe, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, awserr.New(
|
|
ErrCodeProcessProviderExecution,
|
|
errMsgProcessProviderPipe,
|
|
err)
|
|
}
|
|
|
|
p.command.Stderr = os.Stderr // display stderr on console for MFA
|
|
p.command.Stdout = outWritePipe // get creds json on process's stdout
|
|
p.command.Stdin = os.Stdin // enable stdin for MFA
|
|
|
|
output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize))
|
|
|
|
stdoutCh := make(chan error, 1)
|
|
go readInput(
|
|
io.LimitReader(outReadPipe, int64(p.MaxBufSize)),
|
|
output,
|
|
stdoutCh)
|
|
|
|
execCh := make(chan error, 1)
|
|
go executeCommand(*p.command, execCh)
|
|
|
|
finished := false
|
|
var errors []error
|
|
for !finished {
|
|
select {
|
|
case readError := <-stdoutCh:
|
|
errors = appendError(errors, readError)
|
|
finished = true
|
|
case execError := <-execCh:
|
|
err := outWritePipe.Close()
|
|
errors = appendError(errors, err)
|
|
errors = appendError(errors, execError)
|
|
if errors != nil {
|
|
return output.Bytes(), awserr.NewBatchError(
|
|
ErrCodeProcessProviderExecution,
|
|
errMsgProcessProviderProcess,
|
|
errors)
|
|
}
|
|
case <-time.After(p.Timeout):
|
|
finished = true
|
|
return output.Bytes(), awserr.NewBatchError(
|
|
ErrCodeProcessProviderExecution,
|
|
errMsgProcessProviderTimeout,
|
|
errors) // errors can be nil
|
|
}
|
|
}
|
|
|
|
out := output.Bytes()
|
|
|
|
if runtime.GOOS == "windows" {
|
|
// windows adds slashes to quotes
|
|
out = []byte(strings.Replace(string(out), `\"`, `"`, -1))
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// appendError conveniently checks for nil before appending slice
|
|
func appendError(errors []error, err error) []error {
|
|
if err != nil {
|
|
return append(errors, err)
|
|
}
|
|
return errors
|
|
}
|
|
|
|
func executeCommand(cmd exec.Cmd, exec chan error) {
|
|
// Start the command
|
|
err := cmd.Start()
|
|
if err == nil {
|
|
err = cmd.Wait()
|
|
}
|
|
|
|
exec <- err
|
|
}
|
|
|
|
func readInput(r io.Reader, w io.Writer, read chan error) {
|
|
tee := io.TeeReader(r, w)
|
|
|
|
_, err := ioutil.ReadAll(tee)
|
|
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
|
|
read <- err // will only arrive here when write end of pipe is closed
|
|
}
|