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:
parent
80ae331c7d
commit
8eec2a8c06
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue