Browse Source

Enhance IBM Secrets Manager support with kv secretType

Hafid Haddouti 4 years ago
parent
commit
1bbc02daaf

+ 13 - 1
docs/provider-ibm-secrets-manager.md

@@ -49,7 +49,15 @@ See here for a list of [publicly available endpoints](https://cloud.ibm.com/apid
 ![iam-create-success](./pictures/screenshot_service_url.png)
 
 ### Secret Types
-We support all secret types of [IBM Secrets Manager](https://cloud.ibm.com/apidocs/secrets-manager): `arbitrary`, `username_password`, `iam_credentials` and `imported_cert`. To define the type of secret you would like to sync you need to prefix the secret id with the desired type. If the secret type is not specified it is defaulted to `arbitrary`:
+We support the following secret types of [IBM Secrets Manager](https://cloud.ibm.com/apidocs/secrets-manager):
+
+* `arbitrary`, 
+* `username_password`, 
+* `iam_credentials`
+* `imported_cert`
+* and `kv`. 
+
+To define the type of secret you would like to sync you need to prefix the secret id with the desired type. If the secret type is not specified it is defaulted to `arbitrary`:
 
 ```yaml
 {% include 'ibm-es-types.yaml' %}
@@ -75,6 +83,10 @@ The behavior for the different secret types is as following:
 * `remoteRef` requires a `property` to be set for either `certificate`, `private_key` or `intermediate` to retrieve respective fields from the secrets manager secret and set in specified `secretKey`
 * `dataFrom` retrieves all `certificate`, `private_key` and `intermediate` fields from the secrets manager secret and sets appropriate key:value pairs in the resulting Kubernetes secret
 
+#### kv
+* `remoteRef` could has a `property` to select requested key from the KV secret
+* `dataFrom` retrieves a string from secrets manager and tries to parse it as JSON object and setting all key:value pairs or only the requested key in the resulting Kubernetes secret if successful
+
 
 ### Creating external secret
 

+ 3 - 0
docs/snippets/ibm-es-types.yaml

@@ -18,3 +18,6 @@ spec:
   - secretKey: baz
     remoteRef:
       key: imported_cert/zzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz
+  - secretKey: bakv
+    remoteRef:
+      key: kv/zzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz

+ 68 - 0
pkg/provider/ibm/provider.go

@@ -134,6 +134,11 @@ func (ibm *providerIBM) GetSecret(ctx context.Context, ref esv1beta1.ExternalSec
 		}
 
 		return getImportCertSecret(ibm, &secretName, ref)
+
+	case sm.CreateSecretOptionsSecretTypeKvConst:
+
+		return getKVSecret(ibm, &secretName, ref)
+
 	default:
 		return nil, fmt.Errorf("unknown secret type %s", secretType)
 	}
@@ -209,6 +214,50 @@ func getUsernamePasswordSecret(ibm *providerIBM, secretName *string, ref esv1bet
 	return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
 }
 
+func getKVSecret(ibm *providerIBM, secretName *string, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+
+	secret, err := getSecretByType(ibm, secretName, sm.CreateSecretOptionsSecretTypeKvConst, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	secretData := secret.SecretData.(map[string]interface{})
+	secretPayload := secretData["payload"].(string)
+
+	kv := make(map[string]interface{})
+	err = json.Unmarshal([]byte(secretPayload), &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+	}
+
+	// returns only the value of the requested key, otherwise the entire payload
+	if ref.Property != "" {
+
+		if val, ok := kv[ref.Property]; ok {
+			return []byte(val.(string)), nil
+		}
+		return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
+	}
+
+	return []byte(secretPayload), nil
+}
+
+func getSecretByType(ibm *providerIBM, secretName *string, secretType string, ref esv1beta1.ExternalSecretDataRemoteRef) (*sm.SecretResource, error) {
+
+	response, _, err := ibm.IBMClient.GetSecret(
+		&sm.GetSecretOptions{
+			SecretType: core.StringPtr(secretType),
+			ID:         secretName,
+		})
+	if err != nil {
+		return nil, err
+	}
+
+	secret := response.Resources[0].(*sm.SecretResource)
+
+	return secret, nil
+}
+
 func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	if utils.IsNil(ibm.IBMClient) {
 		return nil, fmt.Errorf(errUninitalizedIBMProvider)
@@ -300,6 +349,25 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1beta1.External
 
 		return secretMap, nil
 
+	case sm.CreateSecretOptionsSecretTypeKvConst:
+		secret, err := getSecretByType(ibm, &secretName, sm.CreateSecretOptionsSecretTypeKvConst, ref)
+		if err != nil {
+			return nil, err
+		}
+
+		secretData := secret.SecretData.(map[string]interface{})
+		secretPayload := secretData["payload"].(string)
+
+		kv := make(map[string]interface{})
+		err = json.Unmarshal([]byte(secretPayload), &kv)
+		if err != nil {
+			return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+		}
+
+		secretMap := byteArrayMap(kv)
+
+		return secretMap, nil
+
 	default:
 		return nil, fmt.Errorf("unknown secret type %s", secretType)
 	}

+ 96 - 0
pkg/provider/ibm/provider_test.go

@@ -231,6 +231,78 @@ func TestIBMSecretManagerGetSecret(t *testing.T) {
 		smtc.expectError = "remoteRef.property required for secret type imported_cert"
 	}
 
+	secretDataKV := make(map[string]interface{})
+	secretKVPayload := `{"key1":"val1"}`
+	secretDataKV["payload"] = secretKVPayload
+
+	secretDataKVComplex := make(map[string]interface{})
+	secretKVPayloadComplex := `{"key1":"val1","key2":"val2"}`
+	secretDataKVComplex["payload"] = secretKVPayloadComplex
+
+	secretKV := "kv/test-secret"
+	// bad case: kv type with key which is not in payload
+	badSecretKV := func(smtc *secretManagerTestCase) {
+		resources := []sm.SecretResourceIntf{
+			&sm.SecretResource{
+				SecretType: utilpointer.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst),
+				Name:       utilpointer.StringPtr("testyname"),
+				SecretData: secretDataKV,
+			}}
+
+		smtc.apiInput.SecretType = core.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst)
+		smtc.apiOutput.Resources = resources
+		smtc.ref.Key = secretKV
+		smtc.ref.Property = "other-key"
+		smtc.expectError = "key other-key does not exist in secret kv/test-secret"
+	}
+
+	// good case: kv type with property
+	setSecretKV := func(smtc *secretManagerTestCase) {
+		resources := []sm.SecretResourceIntf{
+			&sm.SecretResource{
+				SecretType: utilpointer.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst),
+				Name:       utilpointer.StringPtr("testyname"),
+				SecretData: secretDataKV,
+			}}
+
+		smtc.apiInput.SecretType = core.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst)
+		smtc.apiOutput.Resources = resources
+		smtc.ref.Key = secretKV
+		smtc.ref.Property = "key1"
+		smtc.expectedSecret = "val1"
+	}
+
+	// good case: kv type without property, returns all
+	setSecretKVWithOutKey := func(smtc *secretManagerTestCase) {
+		resources := []sm.SecretResourceIntf{
+			&sm.SecretResource{
+				SecretType: utilpointer.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst),
+				Name:       utilpointer.StringPtr("testyname"),
+				SecretData: secretDataKV,
+			}}
+
+		smtc.apiInput.SecretType = core.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst)
+		smtc.apiOutput.Resources = resources
+		smtc.ref.Key = secretKV
+		smtc.expectedSecret = secretKVPayload
+	}
+
+	// good case: kv type without property, returns all
+	setSecretKVWithKey := func(smtc *secretManagerTestCase) {
+		resources := []sm.SecretResourceIntf{
+			&sm.SecretResource{
+				SecretType: utilpointer.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst),
+				Name:       utilpointer.StringPtr("testyname"),
+				SecretData: secretDataKVComplex,
+			}}
+
+		smtc.apiInput.SecretType = core.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst)
+		smtc.apiOutput.Resources = resources
+		smtc.ref.Key = secretKV
+		smtc.ref.Property = "key2"
+		smtc.expectedSecret = "val2"
+	}
+
 	successCases := []*secretManagerTestCase{
 		makeValidSecretManagerTestCase(),
 		makeValidSecretManagerTestCaseCustom(setSecretString),
@@ -242,6 +314,10 @@ func TestIBMSecretManagerGetSecret(t *testing.T) {
 		makeValidSecretManagerTestCaseCustom(setSecretIam),
 		makeValidSecretManagerTestCaseCustom(setSecretCert),
 		makeValidSecretManagerTestCaseCustom(badSecretCert),
+		makeValidSecretManagerTestCaseCustom(setSecretKV),
+		makeValidSecretManagerTestCaseCustom(setSecretKVWithOutKey),
+		makeValidSecretManagerTestCaseCustom(setSecretKVWithKey),
+		makeValidSecretManagerTestCaseCustom(badSecretKV),
 	}
 
 	sm := providerIBM{}
@@ -353,6 +429,25 @@ func TestGetSecretMap(t *testing.T) {
 		smtc.expectedData["intermediate"] = []byte(secretIntermediate)
 	}
 
+	// good case: kv
+	setSecretKV := func(smtc *secretManagerTestCase) {
+		secretData := make(map[string]interface{})
+		secretData["payload"] = `{"key1":"val1", "key2":"val2"}`
+
+		resources := []sm.SecretResourceIntf{
+			&sm.SecretResource{
+				SecretType: utilpointer.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst),
+				Name:       utilpointer.StringPtr("testyname"),
+				SecretData: secretData,
+			}}
+
+		smtc.apiInput.SecretType = core.StringPtr(sm.CreateSecretOptionsSecretTypeKvConst)
+		smtc.apiOutput.Resources = resources
+		smtc.ref.Key = "kv/test-secret"
+		smtc.expectedData["key1"] = []byte("val1")
+		smtc.expectedData["key2"] = []byte("val2")
+	}
+
 	successCases := []*secretManagerTestCase{
 		makeValidSecretManagerTestCaseCustom(setDeserialization),
 		makeValidSecretManagerTestCaseCustom(setInvalidJSON),
@@ -361,6 +456,7 @@ func TestGetSecretMap(t *testing.T) {
 		makeValidSecretManagerTestCaseCustom(setSecretUserPass),
 		makeValidSecretManagerTestCaseCustom(setSecretIam),
 		makeValidSecretManagerTestCaseCustom(setSecretCert),
+		makeValidSecretManagerTestCaseCustom(setSecretKV),
 	}
 
 	sm := providerIBM{}