Env secret store: add "prefix" metadata option (+ metadata.yaml) (#2761)

Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
Alessandro (Ale) Segala 2023-04-11 14:00:48 -07:00 committed by GitHub
parent cbe0da4b14
commit 3535c7e9b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 28 deletions

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2021 The Dapr Authors Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -26,8 +26,13 @@ import (
var _ secretstores.SecretStore = (*envSecretStore)(nil) var _ secretstores.SecretStore = (*envSecretStore)(nil)
type Metadata struct {
Prefix string
}
type envSecretStore struct { type envSecretStore struct {
logger logger.Logger logger logger.Logger
metadata Metadata
} }
// NewEnvSecretStore returns a new env var secret store. // 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. // 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 return nil
} }
// GetSecret retrieves a secret from env var using provided key. // GetSecret retrieves a secret from env var using provided key.
func (s *envSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { func (s *envSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) {
var value string var value string
if s.isKeyAllowed(req.Name) { name := s.metadata.Prefix + req.Name
value = os.Getenv(req.Name) if s.isKeyAllowed(name) {
value = os.Getenv(name)
} else { } else {
s.logger.Warnf("Access to env var %s is forbidden", req.Name) 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() env := os.Environ()
r := make(map[string]map[string]string, len(env)) r := make(map[string]map[string]string, len(env))
lp := len(s.metadata.Prefix)
for _, element := range env { for _, element := range env {
envVariable := strings.SplitN(element, "=", 2) envVariable := strings.SplitN(element, "=", 2)
if s.isKeyAllowed(envVariable[0]) { key := envVariable[0]
r[envVariable[0]] = map[string]string{envVariable[0]: envVariable[1]} 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 { func (s *envSecretStore) GetComponentMetadata() map[string]string {
type unusedMetadataStruct struct{} metadataStruct := Metadata{}
metadataStruct := unusedMetadataStruct{}
metadataInfo := map[string]string{} metadataInfo := map[string]string{}
metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType)
return metadataInfo return metadataInfo
} }
func (s *envSecretStore) isKeyAllowed(key string) bool { func (s *envSecretStore) isKeyAllowed(key string) bool {
switch key { key = strings.ToUpper(key)
case "APP_API_TOKEN", "DAPR_API_TOKEN", switch {
"DAPR_TRUST_ANCHORS", "DAPR_CERT_CHAIN", "DAPR_CERT_KEY": case key == "APP_API_TOKEN":
return false
case strings.HasPrefix(key, "DAPR_"):
return false return false
default: default:
return true return true

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2021 The Dapr Authors Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at 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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package env package env
import ( import (
@ -22,18 +23,16 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/components-contrib/secretstores" "github.com/dapr/components-contrib/secretstores"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
func TestEnvStore(t *testing.T) { func TestEnvStore(t *testing.T) {
secret := "secret1"
key := "TEST_SECRET"
s := envSecretStore{logger: logger.NewLogger("test")} s := envSecretStore{logger: logger.NewLogger("test")}
t.Setenv(key, secret) t.Setenv("TEST_SECRET", "secret1")
require.Equal(t, secret, os.Getenv(key)) require.Equal(t, "secret1", os.Getenv("TEST_SECRET"))
t.Run("Init", func(t *testing.T) { t.Run("Init", func(t *testing.T) {
err := s.Init(context.Background(), secretstores.Metadata{}) err := s.Init(context.Background(), secretstores.Metadata{})
@ -41,31 +40,32 @@ func TestEnvStore(t *testing.T) {
}) })
t.Run("Get", func(t *testing.T) { t.Run("Get", func(t *testing.T) {
err := s.Init(context.Background(), secretstores.Metadata{}) resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{Name: "TEST_SECRET"})
require.NoError(t, err)
resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{Name: key})
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, resp) 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) { 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{}) resp, err := s.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{})
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, resp) 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.Run("Disallowed keys", func(t *testing.T) {
t.Setenv("APP_API_TOKEN", "ciao") t.Setenv("APP_API_TOKEN", "ciao")
t.Setenv("DAPR_API_TOKEN", "mondo") t.Setenv("DAPR_API_TOKEN", "mondo")
t.Setenv("dapr_notallowed", "notallowed")
t.Setenv("FOO", "bar") t.Setenv("FOO", "bar")
err := s.Init(context.Background(), secretstores.Metadata{})
require.NoError(t, err)
t.Run("Get", func(t *testing.T) { t.Run("Get", func(t *testing.T) {
resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{ resp, err := s.GetSecret(context.Background(), secretstores.GetSecretRequest{
Name: "APP_API_TOKEN", Name: "APP_API_TOKEN",
@ -73,6 +73,20 @@ func TestEnvStore(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, resp.Data) require.NotNil(t, resp.Data)
assert.Empty(t, resp.Data["APP_API_TOKEN"]) 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) { t.Run("Bulk get", func(t *testing.T) {
@ -81,11 +95,81 @@ func TestEnvStore(t *testing.T) {
require.NotNil(t, resp.Data) require.NotNil(t, resp.Data)
assert.Empty(t, resp.Data["APP_API_TOKEN"]) assert.Empty(t, resp.Data["APP_API_TOKEN"])
assert.Empty(t, resp.Data["DAPR_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"]) 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) { func TestGetFeatures(t *testing.T) {
s := envSecretStore{logger: logger.NewLogger("test")} s := envSecretStore{logger: logger.NewLogger("test")}
// Yes, we are skipping initialization as feature retrieval doesn't depend on it. // Yes, we are skipping initialization as feature retrieval doesn't depend on it.

16
secretstores/local/env/metadata.yaml vendored Normal file
View File

@ -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