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 }