oss/credentials/process_credentials_provider.go (117 lines of code) (raw):
package credentials
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"runtime"
"time"
)
/*
temporary access credentials format
{
"AccessKeyId" : "ak",
"AccessKeySecret" : "sk",
"Expiration" : "2023-12-29T07:45:02Z",
"SecurityToken" : "token",
}
long-term access credentials
{
"AccessKeyId" : "ak",
"AccessKeySecret" : "sk",
}
*/
type processCredentialsResult struct {
AccessKeyId string `json:"AccessKeyId"`
AccessKeySecret string `json:"AccessKeySecret"`
SecurityToken string `json:"SecurityToken"`
Expiration *time.Time `json:"Expiration"`
}
type ProcessCredentialsProviderOptions struct {
Timeout time.Duration
}
type ProcessCredentialsProvider struct {
timeout time.Duration
args []string
}
func NewProcessCredentialsProvider(command string, optFns ...func(*ProcessCredentialsProviderOptions)) CredentialsProvider {
options := ProcessCredentialsProviderOptions{
Timeout: 15 * time.Second,
}
for _, fn := range optFns {
fn(&options)
}
var args []string
if len(command) > 0 {
args = []string{command}
}
return &ProcessCredentialsProvider{
timeout: options.Timeout,
args: args,
}
}
func (p *ProcessCredentialsProvider) GetCredentials(ctx context.Context) (Credentials, error) {
return p.fetchCredentials(ctx)
}
func (p *ProcessCredentialsProvider) buildCommand(ctx context.Context) (*exec.Cmd, error) {
if len(p.args) == 0 {
return nil, fmt.Errorf("command must not be empty")
}
var cmdArgs []string
if runtime.GOOS == "windows" {
cmdArgs = []string{"cmd.exe", "/C"}
} else {
cmdArgs = []string{"sh", "-c"}
}
cmdArgs = append(cmdArgs, p.args...)
cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...)
cmd.Env = os.Environ()
return cmd, nil
}
func (p *ProcessCredentialsProvider) fetchCredentials(ctx context.Context) (Credentials, error) {
data, err := p.executeProcess(ctx)
if err != nil {
return Credentials{}, err
}
//json to Credentials
result := &processCredentialsResult{}
if err = json.Unmarshal(data, result); err != nil {
return Credentials{}, err
}
creds := Credentials{
AccessKeyID: result.AccessKeyId,
AccessKeySecret: result.AccessKeySecret,
SecurityToken: result.SecurityToken,
Expires: result.Expiration,
}
if !creds.HasKeys() {
return creds, fmt.Errorf("missing AccessKeyId or AccessKeySecret in process output")
}
return creds, nil
}
func (p *ProcessCredentialsProvider) executeProcess(ctx context.Context) ([]byte, error) {
if p.timeout >= 0 {
var cancelFunc func()
ctx, cancelFunc = context.WithTimeout(ctx, p.timeout)
defer cancelFunc()
}
cmd, err := p.buildCommand(ctx)
if err != nil {
return nil, err
}
// get creds from process's stdout
output := bytes.NewBuffer(make([]byte, 0, int(8*1024)))
cmd.Stdout = output
// Start the command
executeFn := func(cmd *exec.Cmd, exec chan error) {
err := cmd.Start()
if err == nil {
err = cmd.Wait()
}
exec <- err
}
execCh := make(chan error, 1)
go executeFn(cmd, execCh)
// Wait commnd done
select {
case execError := <-execCh:
if execError == nil {
break
}
select {
case <-ctx.Done():
return output.Bytes(), fmt.Errorf("credential process timed out: %w", execError)
default:
return output.Bytes(), 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
}