SecretStores advertise supported Features(). (#2069)

This PR is aimed at addressing issue #2047.

In the [Secret API Documentation](https://docs.dapr.io/reference/api/secrets_api/#response-body) it is stated:

> If a secret store has support for multiple keys in a secret, a JSON payload is returned with the key names as fields and their respective values.
>
> In case of a secret store that only has name/value semantics, a JSON payload is returned with the name of the secret as the field and the value of the secret as the value.

There are two classes of secret stores but there isn't a way to tell them apart at run-time. This limits the ability of conformance tests to verify the behavior of secret stores supporting multiple keys.

We address this by augmenting SecretStores with the ability to advetise `Features`. This is similar
to what PubSub and StateStores do. Feature `MULTIPLE_KEY_VALUES_PER_SECRET` was added and is
advertised by Hashicorp Vault (default behaviour) and by Local File SecretStore (depending on its configuration).

Updated tests to account to new method and ensure expected behavior.

Fixes #2047

Signed-off-by: Tiago Alves Macambira <tmacam@burocrata.org>

Signed-off-by: Tiago Alves Macambira <tmacam@burocrata.org>
Co-authored-by: Bernd Verst <4535280+berndverst@users.noreply.github.com>
This commit is contained in:
Tiago Alves Macambira 2022-09-13 15:55:39 -07:00 committed by GitHub
parent 80ae331c7d
commit 8eec2a8c06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 186 additions and 0 deletions

View File

@ -184,3 +184,8 @@ func (o *oosSecretStore) getPathFromMetadata(metadata map[string]string) *string
return nil
}
// Features returns the features available in this secret store.
func (o *oosSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{} // No Feature supported.
}

View File

@ -202,3 +202,13 @@ func TestBulkGetSecret(t *testing.T) {
})
})
}
func TestGetFeatures(t *testing.T) {
m := secretstores.Metadata{}
s := NewParameterStore(logger.NewLogger("test"))
s.Init(m)
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

View File

@ -171,3 +171,8 @@ func (s *ssmSecretStore) getSecretManagerMetadata(spec secretstores.Metadata) (*
return &meta, nil
}
// Features returns the features available in this secret store.
func (s *ssmSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{} // No Feature supported.
}

View File

@ -275,3 +275,12 @@ func TestGetBulkSecrets(t *testing.T) {
assert.NotNil(t, err)
})
}
func TestGetFeatures(t *testing.T) {
s := ssmSecretStore{}
// Yes, we are skipping initialization as feature retrieval doesn't depend on it.
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

View File

@ -154,3 +154,8 @@ func (s *smSecretStore) getSecretManagerMetadata(spec secretstores.Metadata) (*s
return &meta, nil
}
// Features returns the features available in this secret store.
func (s *smSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{} // No Feature supported.
}

View File

@ -149,3 +149,11 @@ func TestGetSecret(t *testing.T) {
assert.NotNil(t, err)
})
}
func TestGetFeatures(t *testing.T) {
s := smSecretStore{}
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

View File

@ -197,3 +197,8 @@ func (k *keyvaultSecretStore) getMaxResultsFromMetadata(metadata map[string]stri
return nil, nil
}
// Features returns the features available in this secret store.
func (k *keyvaultSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{} // No Feature supported.
}

View File

@ -88,3 +88,12 @@ func TestInit(t *testing.T) {
assert.NotNil(t, kv.vaultClient)
})
}
func TestGetFeatures(t *testing.T) {
s := NewAzureKeyvaultSecretStore(logger.NewLogger("test"))
// Yes, we are skipping initialization as feature retrieval doesn't depend on it.
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

33
secretstores/feature.go Normal file
View File

@ -0,0 +1,33 @@
/*
Copyright 2022 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package secretstores
// Feature names a feature that can be implemented by Secret Store components.
type Feature string
const (
// FeatureMultipleKeyValuesPerSecret advertises that this SecretStore supports multiple keys-values under a single secret.
FeatureMultipleKeyValuesPerSecret Feature = "MULTIPLE_KEY_VALUES_PER_SECRET"
)
// IsPresent checks if a given feature is present in the list.
func (f Feature) IsPresent(features []Feature) bool {
for _, feature := range features {
if feature == f {
return true
}
}
return false
}

View File

@ -196,3 +196,8 @@ func (s *Store) parseSecretManagerMetadata(metadataRaw secretstores.Metadata) (*
return &meta, nil
}
// Features returns the features available in this secret store.
func (s *Store) Features() []secretstores.Feature {
return []secretstores.Feature{} // No Feature supported.
}

View File

@ -130,3 +130,12 @@ func TestBulkGetSecret(t *testing.T) {
assert.Equal(t, secretstores.BulkGetSecretResponse{Data: nil}, v)
})
}
func TestGetFeatures(t *testing.T) {
s := NewSecreteManager(logger.NewLogger("test"))
// Yes, we are skipping initialization as feature retrieval doesn't depend on it.
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

View File

@ -503,3 +503,8 @@ func readCertificateFolder(certPool *x509.CertPool, path string) error {
return nil
}
// Features returns the features available in this secret store.
func (v *vaultSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{secretstores.FeatureMultipleKeyValuesPerSecret}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/components-contrib/secretstores"
"github.com/dapr/kit/logger"
)
const (
@ -406,3 +407,12 @@ func getCertificate() []byte {
return certificateBytes
}
func TestGetFeatures(t *testing.T) {
s := NewHashiCorpVaultSecretStore(logger.NewLogger("test"))
// Yes, we are skipping initialization as feature retrieval doesn't depend on it.
t.Run("Vault supports MULTIPLE_KEY_VALUES_PER_SECRET", func(t *testing.T) {
f := s.Features()
assert.True(t, secretstores.FeatureMultipleKeyValuesPerSecret.IsPresent(f))
})
}

View File

@ -140,3 +140,8 @@ func (c *csmsSecretStore) getSecretNames(marker *string) ([]string, error) {
return resp, nil
}
// Features returns the features available in this secret store.
func (c *csmsSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{} // No Feature supported.
}

View File

@ -161,3 +161,14 @@ func TestBulkGetSecret(t *testing.T) {
})
})
}
func TestGetFeatures(t *testing.T) {
s := csmsSecretStore{
client: &mockedCsmsSecretStore{},
}
// Yes, we are skipping initialization as feature retrieval doesn't depend on it.
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

View File

@ -107,3 +107,8 @@ func (k *kubernetesSecretStore) getNamespaceFromMetadata(metadata map[string]str
return "", errors.New("namespace is missing on metadata and NAMESPACE env variable")
}
// Features returns the features available in this secret store.
func (k *kubernetesSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{}
}

View File

@ -50,3 +50,12 @@ func TestGetNamespace(t *testing.T) {
assert.Equal(t, "namespace is missing on metadata and NAMESPACE env variable", err.Error())
})
}
func TestGetFeatures(t *testing.T) {
s := kubernetesSecretStore{logger: logger.NewLogger("test")}
// Yes, we are skipping initialization as feature retrieval doesn't depend on it.
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

View File

@ -59,3 +59,8 @@ func (s *envSecretStore) BulkGetSecret(req secretstores.BulkGetSecretRequest) (s
Data: r,
}, nil
}
// Features returns the features available in this secret store.
func (s *envSecretStore) Features() []secretstores.Feature {
return []secretstores.Feature{} // No Feature supported.
}

View File

@ -56,3 +56,12 @@ func TestInit(t *testing.T) {
assert.Equal(t, secret, resp.Data[key][key])
})
}
func TestGetFeatures(t *testing.T) {
s := envSecretStore{logger: logger.NewLogger("test")}
// Yes, we are skipping initialization as feature retrieval doesn't depend on it.
t.Run("no features are advertised", func(t *testing.T) {
f := s.Features()
assert.Empty(t, f)
})
}

View File

@ -41,6 +41,7 @@ type localSecretStore struct {
currentPath string
secrets map[string]interface{}
readLocalFileFn func(secretsFile string) (map[string]interface{}, error)
features []secretstores.Feature
logger logger.Logger
}
@ -86,9 +87,17 @@ func (j *localSecretStore) Init(metadata secretstores.Metadata) error {
}
}
j.secrets = allSecrets
// If MultiValued is set, this secret store supports a multiple
// key-valyes per secret.
j.features = []secretstores.Feature{
secretstores.FeatureMultipleKeyValuesPerSecret,
}
} else {
j.secrets = map[string]interface{}{}
j.visitJSONObject(jsonConfig)
// MultiValued is not set: reset to its default single-value per
// secret (no extra feature) behavior.
j.features = []secretstores.Feature{}
}
return nil
@ -259,3 +268,8 @@ func (j *localSecretStore) readLocalFile(secretsFile string) (map[string]interfa
return jsonConfig, nil
}
// Features returns the features available in this secret store.
func (j *localSecretStore) Features() []secretstores.Feature {
return j.features
}

View File

@ -138,6 +138,10 @@ func TestGetSecret(t *testing.T) {
assert.NotNil(t, err)
assert.Equal(t, err, fmt.Errorf("secret %s not found", req.Name))
})
t.Run("Regular (non-MultiValued) secret store does not support MULTIPLE_KEY_VALUES_PER_SECRET", func(t *testing.T) {
assert.False(t, secretstores.FeatureMultipleKeyValuesPerSecret.IsPresent(s.Features()))
})
}
func TestBulkGetSecret(t *testing.T) {
@ -195,6 +199,10 @@ func TestMultiValuedSecrets(t *testing.T) {
err := s.Init(m)
require.NoError(t, err)
t.Run("MultiValued stores support MULTIPLE_KEY_VALUES_PER_SECRET", func(t *testing.T) {
assert.True(t, secretstores.FeatureMultipleKeyValuesPerSecret.IsPresent(s.Features()))
})
t.Run("successfully retrieve a single multi-valued secret", func(t *testing.T) {
req := secretstores.GetSecretRequest{
Name: "parent",

View File

@ -27,6 +27,8 @@ type SecretStore interface {
GetSecret(req GetSecretRequest) (GetSecretResponse, error)
// BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values.
BulkGetSecret(req BulkGetSecretRequest) (BulkGetSecretResponse, error)
// Features lists the features supported by the secret store.
Features() []Feature
}
func Ping(secretStore SecretStore) error {