From fa55bceb5f0695af21362c0ab2b813834ccd7973 Mon Sep 17 00:00:00 2001
From: Ondrej Babec <obabec@dnanexus.com>
Date: Tue, 26 Nov 2024 16:45:35 +0100
Subject: [PATCH] Support raw/plain secret pull from AWS Secret Manager

---
 template/funcs.go                             | 22 ++++++++++++-
 .../aws/secretsmanager/secretsmanager.go      | 10 +++---
 .../aws/secretsmanager/secretsmanager_test.go | 33 ++++++++++++++++++-
 3 files changed, 58 insertions(+), 7 deletions(-)

diff --git a/template/funcs.go b/template/funcs.go
index 13cd2e344..61e3ff985 100644
--- a/template/funcs.go
+++ b/template/funcs.go
@@ -117,5 +117,25 @@ func GetAWSSecret(name, key string) (string, error) {
 		Key:  key,
 	}
 
-	return client.GetSecret(spec)
+	return client.GetSecret(spec, false)
+}
+
+func GetRawAWSSecret(name string) (string, error) {
+	if len(name) == 0 {
+		return "", errors.New("At least one secret name must be provided")
+	}
+
+	// client uses AWS SDK CredentialChain method. So,credentials can
+	// be loaded from credential file, environment variables, or IAM
+	// roles.
+	client := awssmapi.New(
+		&awssmapi.AWSConfig{},
+	)
+
+	spec := &awssmapi.SecretSpec{
+		Name: name,
+		Key:  "",
+	}
+
+	return client.GetSecret(spec, true)
 }
diff --git a/template/interpolate/aws/secretsmanager/secretsmanager.go b/template/interpolate/aws/secretsmanager/secretsmanager.go
index 9fd0d18f3..1f7e3a379 100644
--- a/template/interpolate/aws/secretsmanager/secretsmanager.go
+++ b/template/interpolate/aws/secretsmanager/secretsmanager.go
@@ -52,7 +52,7 @@ func (c *Client) newSession(config *AWSConfig) *session.Session {
 
 // GetSecret return an AWS Secret Manager secret
 // in plain text from a given secret name
-func (c *Client) GetSecret(spec *SecretSpec) (string, error) {
+func (c *Client) GetSecret(spec *SecretSpec, raw bool) (string, error) {
 	params := &secretsmanager.GetSecretValueInput{
 		SecretId:     aws.String(spec.Name),
 		VersionStage: aws.String("AWSCURRENT"),
@@ -71,7 +71,7 @@ func (c *Client) GetSecret(spec *SecretSpec) (string, error) {
 		Name:         *resp.Name,
 		SecretString: *resp.SecretString,
 	}
-	value, err := getSecretValue(&secret, spec)
+	value, err := getSecretValue(&secret, spec, raw)
 	if err != nil {
 		return "", err
 	}
@@ -79,12 +79,12 @@ func (c *Client) GetSecret(spec *SecretSpec) (string, error) {
 	return value, nil
 }
 
-func getSecretValue(s *SecretString, spec *SecretSpec) (string, error) {
+func getSecretValue(s *SecretString, spec *SecretSpec, raw bool) (string, error) {
 	var secretValue map[string]interface{}
 	blob := []byte(s.SecretString)
 
-	//For those plaintext secrets just return the value
-	if !json.Valid(blob) {
+	//For those plaintext secrets just return the value or if raw is requested
+	if !json.Valid(blob) || raw {
 		return s.SecretString, nil
 	}
 
diff --git a/template/interpolate/aws/secretsmanager/secretsmanager_test.go b/template/interpolate/aws/secretsmanager/secretsmanager_test.go
index a2d872a5a..503f939cc 100644
--- a/template/interpolate/aws/secretsmanager/secretsmanager_test.go
+++ b/template/interpolate/aws/secretsmanager/secretsmanager_test.go
@@ -25,6 +25,7 @@ func TestGetSecret(t *testing.T) {
 	testCases := []struct {
 		description string
 		arg         *SecretSpec
+		raw         bool
 		mock        secretsmanager.GetSecretValueOutput
 		want        string
 		ok          bool
@@ -32,6 +33,7 @@ func TestGetSecret(t *testing.T) {
 		{
 			description: "input has valid secret name, secret has single key",
 			arg:         &SecretSpec{Name: "test/secret"},
+			raw:         false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test/secret"),
 				SecretString: aws.String(`{"key": "test"}`),
@@ -45,6 +47,7 @@ func TestGetSecret(t *testing.T) {
 				Name: "test/secret",
 				Key:  "key",
 			},
+			raw: false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test/secret"),
 				SecretString: aws.String(`{"key": "test"}`),
@@ -58,6 +61,7 @@ func TestGetSecret(t *testing.T) {
 				Name: "test/secret",
 				Key:  "second_key",
 			},
+			raw: false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test/secret"),
 				SecretString: aws.String(`{"first_key": "first_val", "second_key": "second_val"}`),
@@ -70,6 +74,7 @@ func TestGetSecret(t *testing.T) {
 			arg: &SecretSpec{
 				Name: "test/secret",
 			},
+			raw: false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test/secret"),
 				SecretString: aws.String(`{"first_key": "first_val", "second_key": "second_val"}`),
@@ -82,6 +87,7 @@ func TestGetSecret(t *testing.T) {
 				Name: "test/secret",
 				Key:  "nonexistent",
 			},
+			raw: false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test/secret"),
 				SecretString: aws.String(`{"key": "test"}`),
@@ -94,6 +100,7 @@ func TestGetSecret(t *testing.T) {
 				Name: "test/secret",
 				Key:  "nonexistent",
 			},
+			raw: false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test/secret"),
 				SecretString: aws.String(`{"first_key": "first_val", "second_key": "second_val"}`),
@@ -106,6 +113,7 @@ func TestGetSecret(t *testing.T) {
 				Name: "test/secret",
 				Key:  "nonexistent",
 			},
+			raw:  false,
 			mock: secretsmanager.GetSecretValueOutput{},
 			ok:   false,
 		},
@@ -114,6 +122,7 @@ func TestGetSecret(t *testing.T) {
 			arg: &SecretSpec{
 				Name: "test",
 			},
+			raw: false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test"),
 				SecretString: aws.String("ThisIsThePassword"),
@@ -124,6 +133,7 @@ func TestGetSecret(t *testing.T) {
 		{
 			description: "input as secret stored with 'String: int' value",
 			arg:         &SecretSpec{Name: "test"},
+			raw:         false,
 			mock: secretsmanager.GetSecretValueOutput{
 				Name:         aws.String("test"),
 				SecretString: aws.String(`{"port": 5432}`),
@@ -131,13 +141,34 @@ func TestGetSecret(t *testing.T) {
 			want: "5432",
 			ok:   true,
 		},
+		{
+			description: "input as secret stored as json object, returned as json",
+			arg:         &SecretSpec{Name: "test"},
+			raw:         true,
+			mock: secretsmanager.GetSecretValueOutput{
+				Name:         aws.String("test"),
+				SecretString: aws.String(`{"foo":{"bar":"baz"}}`),
+			},
+			want: `{"foo":{"bar":"baz"}}`,
+			ok:   true,
+		},
+		{
+			description: "input as secret stored as json with object, fails without raw",
+			arg:         &SecretSpec{Name: "test"},
+			raw:         false,
+			mock: secretsmanager.GetSecretValueOutput{
+				Name:         aws.String("test"),
+				SecretString: aws.String(`{"foo":{"bar":"baz"}}`),
+			},
+			ok: false,
+		},
 	}
 
 	for _, test := range testCases {
 		c := &Client{
 			api: mockedSecret{Resp: test.mock},
 		}
-		got, err := c.GetSecret(test.arg)
+		got, err := c.GetSecret(test.arg, test.raw)
 		if test.ok {
 			if got != test.want {
 				t.Fatalf("want %v, got %v, error %v, using arg %v", test.want, got, err, test.arg)