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