diff --git a/secretstores/local/env/envstore.go b/secretstores/local/env/envstore.go index 4f526cf54..cf0c95cf2 100644 --- a/secretstores/local/env/envstore.go +++ b/secretstores/local/env/envstore.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +Copyright 2023 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 @@ -26,8 +26,13 @@ import ( var _ secretstores.SecretStore = (*envSecretStore)(nil) +type Metadata struct { + Prefix string +} + type envSecretStore struct { - logger logger.Logger + logger logger.Logger + metadata Metadata } // NewEnvSecretStore returns a new env var secret store. @@ -38,15 +43,19 @@ func NewEnvSecretStore(logger logger.Logger) secretstores.SecretStore { } // Init creates a Local secret store. -func (s *envSecretStore) Init(_ context.Context, metadata secretstores.Metadata) error { +func (s *envSecretStore) Init(_ context.Context, meta secretstores.Metadata) error { + if err := metadata.DecodeMetadata(meta.Properties, &s.metadata); err != nil { + return err + } return nil } // GetSecret retrieves a secret from env var using provided key. func (s *envSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { var value string - if s.isKeyAllowed(req.Name) { - value = os.Getenv(req.Name) + name := s.metadata.Prefix + req.Name + if s.isKeyAllowed(name) { + value = os.Getenv(name) } else { s.logger.Warnf("Access to env var %s is forbidden", req.Name) } @@ -62,10 +71,15 @@ func (s *envSecretStore) BulkGetSecret(ctx context.Context, req secretstores.Bul env := os.Environ() r := make(map[string]map[string]string, len(env)) + lp := len(s.metadata.Prefix) for _, element := range env { envVariable := strings.SplitN(element, "=", 2) - if s.isKeyAllowed(envVariable[0]) { - r[envVariable[0]] = map[string]string{envVariable[0]: envVariable[1]} + key := envVariable[0] + if s.metadata.Prefix != "" && !strings.HasPrefix(key, s.metadata.Prefix) { + continue + } + if s.isKeyAllowed(key) { + r[key[lp:]] = map[string]string{key[lp:]: envVariable[1]} } } @@ -80,17 +94,18 @@ func (s *envSecretStore) Features() []secretstores.Feature { } func (s *envSecretStore) GetComponentMetadata() map[string]string { - type unusedMetadataStruct struct{} - metadataStruct := unusedMetadataStruct{} + metadataStruct := Metadata{} metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) return metadataInfo } func (s *envSecretStore) isKeyAllowed(key string) bool { - switch key { - case "APP_API_TOKEN", "DAPR_API_TOKEN", - "DAPR_TRUST_ANCHORS", "DAPR_CERT_CHAIN", "DAPR_CERT_KEY": + key = strings.ToUpper(key) + switch { + case key == "APP_API_TOKEN": + return false + case strings.HasPrefix(key, "DAPR_"): return false default: return true diff --git a/secretstores/local/env/envstore_test.go b/secretstores/local/env/envstore_test.go index 79ee3d460..983ceb62b 100644 --- a/secretstores/local/env/envstore_test.go +++ b/secretstores/local/env/envstore_test.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +Copyright 2023 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 @@ -12,6 +12,7 @@ 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 env import ( @@ -22,18 +23,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/secretstores" "github.com/dapr/kit/logger" ) func TestEnvStore(t *testing.T) { - secret := "secret1" - key := "TEST_SECRET" - s := envSecretStore{logger: logger.NewLogger("test")} - t.Setenv(key, secret) - require.Equal(t, secret, os.Getenv(key)) + t.Setenv("TEST_SECRET", "secret1") + require.Equal(t, "secret1", os.Getenv("TEST_SECRET")) t.Run("Init", func(t *testing.T) { err := s.Init(context.Background(), secretstores.Metadata{}) @@ -41,31 +40,32 @@ func TestEnvStore(t *testing.T) { }) t.Run("Get", func(t *testing.T) { - err := s.Init(context.Background(), secretstores.Metadata{}) - require.NoError(t, err) - resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{Name: key}) + resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{Name: "TEST_SECRET"}) require.NoError(t, err) require.NotNil(t, resp) - assert.Equal(t, secret, resp.Data[key]) + assert.Equal(t, "secret1", resp.Data["TEST_SECRET"]) + }) + + t.Run("Get case sensitive", func(t *testing.T) { + resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{Name: "test_secret"}) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Empty(t, resp.Data["test_secret"]) }) t.Run("Bulk get", func(t *testing.T) { - err := s.Init(context.Background(), secretstores.Metadata{}) - require.NoError(t, err) resp, err := s.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) require.NoError(t, err) require.NotNil(t, resp) - assert.Equal(t, secret, resp.Data[key][key]) + assert.Equal(t, "secret1", resp.Data["TEST_SECRET"]["TEST_SECRET"]) }) t.Run("Disallowed keys", func(t *testing.T) { t.Setenv("APP_API_TOKEN", "ciao") t.Setenv("DAPR_API_TOKEN", "mondo") + t.Setenv("dapr_notallowed", "notallowed") t.Setenv("FOO", "bar") - err := s.Init(context.Background(), secretstores.Metadata{}) - require.NoError(t, err) - t.Run("Get", func(t *testing.T) { resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{ Name: "APP_API_TOKEN", @@ -73,6 +73,20 @@ func TestEnvStore(t *testing.T) { require.NoError(t, err) require.NotNil(t, resp.Data) assert.Empty(t, resp.Data["APP_API_TOKEN"]) + + resp, err = s.GetSecret(context.Background(), secretstores.GetSecretRequest{ + Name: "dapr_notallowed", + }) + require.NoError(t, err) + require.NotNil(t, resp.Data) + assert.Empty(t, resp.Data["dapr_notallowed"]) + + resp, err = s.GetSecret(context.Background(), secretstores.GetSecretRequest{ + Name: "DAPR_NOTALLOWED", + }) + require.NoError(t, err) + require.NotNil(t, resp.Data) + assert.Empty(t, resp.Data["DAPR_NOTALLOWED"]) }) t.Run("Bulk get", func(t *testing.T) { @@ -81,11 +95,81 @@ func TestEnvStore(t *testing.T) { require.NotNil(t, resp.Data) assert.Empty(t, resp.Data["APP_API_TOKEN"]) assert.Empty(t, resp.Data["DAPR_API_TOKEN"]) + assert.Empty(t, resp.Data["DAPR_NOTALLOWED"]) assert.Equal(t, "bar", resp.Data["FOO"]["FOO"]) }) }) } +func TestEnvStoreWithPrefix(t *testing.T) { + s := envSecretStore{logger: logger.NewLogger("test")} + + t.Setenv("TEST_SECRET", "test1") + t.Setenv("test_secret2", "test2") + t.Setenv("FOO_SECRET", "foo1") + require.Equal(t, "test1", os.Getenv("TEST_SECRET")) + require.Equal(t, "test2", os.Getenv("test_secret2")) + require.Equal(t, "foo1", os.Getenv("FOO_SECRET")) + + t.Run("Get", func(t *testing.T) { + err := s.Init(context.Background(), secretstores.Metadata{ + Base: metadata.Base{Properties: map[string]string{ + // Prefix should be case-sensitive + "prefix": "TEST_", + }}, + }) + require.NoError(t, err) + + resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{Name: "SECRET"}) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Len(t, resp.Data, 1) + assert.Equal(t, "test1", resp.Data["SECRET"]) + }) + + t.Run("Bulk get", func(t *testing.T) { + err := s.Init(context.Background(), secretstores.Metadata{ + Base: metadata.Base{Properties: map[string]string{ + "prefix": "TEST_", + }}, + }) + require.NoError(t, err) + + resp, err := s.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Len(t, resp.Data, 1) + assert.Equal(t, "test1", resp.Data["SECRET"]["SECRET"]) + }) + + t.Run("Disallowed keys", func(t *testing.T) { + t.Setenv("DAPR_API_TOKEN", "hi") + + t.Run("Get", func(t *testing.T) { + err := s.Init(context.Background(), secretstores.Metadata{ + Base: metadata.Base{Properties: map[string]string{ + "prefix": "DAPR_", + }}, + }) + require.NoError(t, err) + + resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{ + Name: "API_TOKEN", + }) + require.NoError(t, err) + require.NotNil(t, resp.Data) + assert.Empty(t, resp.Data["API_TOKEN"]) + }) + + t.Run("Bulk get", func(t *testing.T) { + resp, err := s.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + require.NotNil(t, resp.Data) + assert.Empty(t, resp.Data["API_TOKEN"]) + }) + }) +} + func TestGetFeatures(t *testing.T) { s := envSecretStore{logger: logger.NewLogger("test")} // Yes, we are skipping initialization as feature retrieval doesn't depend on it. diff --git a/secretstores/local/env/metadata.yaml b/secretstores/local/env/metadata.yaml new file mode 100644 index 000000000..ff848a8ec --- /dev/null +++ b/secretstores/local/env/metadata.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: secretstores +name: local.env +version: v1 +status: stable +title: "Local environment variables" +urls: + - title: Reference + url: "https://docs.dapr.io/reference/components-reference/supported-secret-stores/envvar-secret-store/" +metadata: + - name: prefix + description: | + If set, limits operations to environmental variables with the given prefix. The prefix is case-sensitive and is removed from the returned secrets' names. + example: '"MYAPP_"' + type: string