Repository: minio/minio-go (go)

Issue:
Missing support for credential_process
minio-go can obtain credentials from a configured aws config file.

This process, however, only looks for static credentials and does not support the [credential_process](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html) mechanism to execute an external program that obtains the required credentials.

This is useful in environments where federated authentication is in place and long-living credentials are forbidden.

Pull request description:
Add support for credential_process
Fixes #1485

Signed-off-by: Harshavardhana <harsha@minio.io>

Patch:
diff --git a/pkg/credentials/credentials.json b/pkg/credentials/credentials.json
new file mode 100644
index 0000000..afbfad5
--- /dev/null
+++ b/pkg/credentials/credentials.json
@@ -0,0 +1,7 @@
+{
+    "Version": 1,
+    "SessionToken": "token",
+    "AccessKeyId": "accessKey",
+    "SecretAccessKey": "secret",
+    "Expiration": "9999-04-27T16:02:25.000Z"
+}
diff --git a/pkg/credentials/credentials.sample b/pkg/credentials/credentials.sample
index 7fc91d9..e2dc1bf 100644
--- a/pkg/credentials/credentials.sample
+++ b/pkg/credentials/credentials.sample
@@ -10,3 +10,6 @@ aws_secret_access_key = secret
 [with_colon]
 aws_access_key_id: accessKey
 aws_secret_access_key: secret
+
+[with_process]
+credential_process = /bin/cat credentials.json
diff --git a/pkg/credentials/file_aws_credentials.go b/pkg/credentials/file_aws_credentials.go
index cbdcfe2..da09707 100644
--- a/pkg/credentials/file_aws_credentials.go
+++ b/pkg/credentials/file_aws_credentials.go
@@ -18,17 +18,33 @@
 package credentials
 
 import (
+	"encoding/json"
+	"errors"
 	"os"
+	"os/exec"
 	"path/filepath"
+	"strings"
+	"time"
 
 	ini "gopkg.in/ini.v1"
 )
 
+// A externalProcessCredentials stores the output of a credential_process
+type externalProcessCredentials struct {
+	Version         int
+	SessionToken    string
+	AccessKeyID     string `json:"AccessKeyId"`
+	SecretAccessKey string
+	Expiration      time.Time
+}
+
 // A FileAWSCredentials retrieves credentials from the current user's home
 // directory, and keeps track if those credentials are expired.
 //
 // Profile ini file example: $HOME/.aws/credentials
 type FileAWSCredentials struct {
+	Expiry
+
 	// Path to the shared credentials file.
 	//
 	// If empty will look for "AWS_SHARED_CREDENTIALS_FILE" env variable. If the
@@ -89,6 +105,33 @@ func (p *FileAWSCredentials) Retrieve() (Value, error) {
 	// Default to empty string if not found.
 	token := iniProfile.Key("aws_session_token")
 
+	// If credential_process is defined, obtain credentials by executing
+	// the external process
+	credentialProcess := strings.TrimSpace(iniProfile.Key("credential_process").String())
+	if credentialProcess != "" {
+		args := strings.Fields(credentialProcess)
+		if len(args) <= 1 {
+			return Value{}, errors.New("invalid credential process args")
+		}
+		cmd := exec.Command(args[0], args[1:]...)
+		out, err := cmd.Output()
+		if err != nil {
+			return Value{}, err
+		}
+		var externalProcessCredentials externalProcessCredentials
+		err = json.Unmarshal([]byte(out), &externalProcessCredentials)
+		if err != nil {
+			return Value{}, err
+		}
+		p.retrieved = true
+		p.SetExpiration(externalProcessCredentials.Expiration, DefaultExpiryWindow)
+		return Value{
+			AccessKeyID:     externalProcessCredentials.AccessKeyID,
+			SecretAccessKey: externalProcessCredentials.SecretAccessKey,
+			SessionToken:    externalProcessCredentials.SessionToken,
+			SignerType:      SignatureV4,
+		}, nil
+	}
 	p.retrieved = true
 	return Value{
 		AccessKeyID:     id.String(),
@@ -98,11 +141,6 @@ func (p *FileAWSCredentials) Retrieve() (Value, error) {
 	}, nil
 }
 
-// IsExpired returns if the shared credentials have expired.
-func (p *FileAWSCredentials) IsExpired() bool {
-	return !p.retrieved
-}
-
 // loadProfiles loads from the file pointed to by shared credentials filename for profile.
 // The credentials retrieved from the profile will be returned or error. Error will be
 // returned if it fails to read from the file, or the data is invalid.


Test patch:
diff --git a/pkg/credentials/file_test.go b/pkg/credentials/file_test.go
index 598c3f5..d42fd3a 100644
--- a/pkg/credentials/file_test.go
+++ b/pkg/credentials/file_test.go
@@ -120,6 +120,27 @@ func TestFileAWS(t *testing.T) {
 	if !creds.IsExpired() {
 		t.Error("Should be expired if not loaded")
 	}
+
+	os.Clearenv()
+
+	creds = NewFileAWSCredentials("credentials.sample", "with_process")
+	credValues, err = creds.Get()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if credValues.AccessKeyID != "accessKey" {
+		t.Errorf("Expected 'accessKey', got %s'", credValues.AccessKeyID)
+	}
+	if credValues.SecretAccessKey != "secret" {
+		t.Errorf("Expected 'secret', got %s'", credValues.SecretAccessKey)
+	}
+	if credValues.SessionToken != "token" {
+		t.Errorf("Expected 'token', got %s'", credValues.SessionToken)
+	}
+	if creds.IsExpired() {
+		t.Error("Should not be expired")
+	}
 }
 
 func TestFileMinioClient(t *testing.T) {