diff --git a/bootstrap/environment/variables_test.go b/bootstrap/environment/variables_test.go index 2965030b..a13593c0 100644 --- a/bootstrap/environment/variables_test.go +++ b/bootstrap/environment/variables_test.go @@ -21,16 +21,14 @@ import ( "strconv" "testing" - "github.com/edgexfoundry/go-mod-secrets/pkg/providers/vault" - "github.com/stretchr/testify/require" + "github.com/edgexfoundry/go-mod-bootstrap/config" "github.com/edgexfoundry/go-mod-configuration/pkg/types" - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + secretsTypes "github.com/edgexfoundry/go-mod-secrets/pkg/types" "github.com/stretchr/testify/assert" - - "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/stretchr/testify/require" ) const ( @@ -327,7 +325,7 @@ func TestOverrideConfigurationExactCase(t *testing.T) { List: []string{"val1"}, FloatVal: float32(11.11), SecretStore: config.SecretStoreInfo{ - Authentication: vault.AuthenticationInfo{ + Authentication: secretsTypes.AuthenticationInfo{ AuthType: "none", }, }, @@ -383,7 +381,7 @@ func TestOverrideConfigurationUppercase(t *testing.T) { List: []string{"val1"}, FloatVal: float32(11.11), SecretStore: config.SecretStoreInfo{ - Authentication: vault.AuthenticationInfo{ + Authentication: secretsTypes.AuthenticationInfo{ AuthType: "none", AuthToken: expectedAuthToken, }, @@ -432,7 +430,7 @@ func TestOverrideConfigurationWithBlankValue(t *testing.T) { List: []string{"val1"}, FloatVal: float32(11.11), SecretStore: config.SecretStoreInfo{ - Authentication: vault.AuthenticationInfo{ + Authentication: secretsTypes.AuthenticationInfo{ AuthType: "none", AuthToken: expectedAuthToken, }, @@ -463,7 +461,7 @@ func TestOverrideConfigurationWithEqualInValue(t *testing.T) { SecretStore config.SecretStoreInfo }{ SecretStore: config.SecretStoreInfo{ - Authentication: vault.AuthenticationInfo{ + Authentication: secretsTypes.AuthenticationInfo{ AuthType: "none", AuthToken: expectedAuthToken, }, diff --git a/bootstrap/handlers/httpserver/httpserver.go b/bootstrap/handlers/httpserver.go similarity index 94% rename from bootstrap/handlers/httpserver/httpserver.go rename to bootstrap/handlers/httpserver.go index 576e89b3..f003026d 100644 --- a/bootstrap/handlers/httpserver/httpserver.go +++ b/bootstrap/handlers/httpserver.go @@ -12,7 +12,7 @@ * the License. *******************************************************************************/ -package httpserver +package handlers import ( "context" @@ -35,8 +35,8 @@ type HttpServer struct { doListenAndServe bool } -// NewBootstrap is a factory method that returns an initialized HttpServer receiver struct. -func NewBootstrap(router *mux.Router, doListenAndServe bool) *HttpServer { +// NewHttpServer is a factory method that returns an initialized HttpServer receiver struct. +func NewHttpServer(router *mux.Router, doListenAndServe bool) *HttpServer { return &HttpServer{ router: router, isRunning: false, diff --git a/bootstrap/handlers/message/message.go b/bootstrap/handlers/message.go similarity index 91% rename from bootstrap/handlers/message/message.go rename to bootstrap/handlers/message.go index 2679fba7..8b912027 100644 --- a/bootstrap/handlers/message/message.go +++ b/bootstrap/handlers/message.go @@ -12,7 +12,7 @@ * the License. *******************************************************************************/ -package message +package handlers import ( "context" @@ -30,8 +30,8 @@ type StartMessage struct { version string } -// NewBootstrap is a factory method that returns an initialized StartMessage receiver struct. -func NewBootstrap(serviceKey, version string) *StartMessage { +// NewStartMessage is a factory method that returns an initialized StartMessage receiver struct. +func NewStartMessage(serviceKey, version string) *StartMessage { return &StartMessage{ serviceKey: serviceKey, version: version, diff --git a/bootstrap/handlers/testing/ready.go b/bootstrap/handlers/ready.go similarity index 92% rename from bootstrap/handlers/testing/ready.go rename to bootstrap/handlers/ready.go index 040b644a..400de994 100644 --- a/bootstrap/handlers/testing/ready.go +++ b/bootstrap/handlers/ready.go @@ -12,7 +12,7 @@ * the License. *******************************************************************************/ -package testing +package handlers import ( "context" @@ -33,8 +33,8 @@ type Ready struct { stream chan<- bool } -// NewBootstrap is a factory method that returns an initialized Ready receiver struct. -func NewBootstrap(httpServer httpServer, stream chan<- bool) *Ready { +// NewReady is a factory method that returns an initialized Ready receiver struct. +func NewReady(httpServer httpServer, stream chan<- bool) *Ready { return &Ready{ httpServer: httpServer, stream: stream, diff --git a/bootstrap/secret/handler.go b/bootstrap/handlers/secret.go similarity index 61% rename from bootstrap/secret/handler.go rename to bootstrap/handlers/secret.go index 362e91d3..b555de64 100644 --- a/bootstrap/secret/handler.go +++ b/bootstrap/handlers/secret.go @@ -12,7 +12,7 @@ * the License. *******************************************************************************/ -package secret +package handlers import ( "context" @@ -20,6 +20,8 @@ import ( "sync" "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/secret" "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" "github.com/edgexfoundry/go-mod-bootstrap/config" "github.com/edgexfoundry/go-mod-bootstrap/di" @@ -30,55 +32,67 @@ import ( "github.com/edgexfoundry/go-mod-secrets/pkg/token/fileioperformer" ) -// BootstrapHandler full initializes the Provider store manager. -func (p *Provider) BootstrapHandler( +// SecureProviderBootstrapHandler full initializes the Secret Provider. +func SecureProviderBootstrapHandler( ctx context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { + lc := container.LoggingClientFrom(dic.Get) + configuration := container.ConfigurationFrom(dic.Get) - p.lc = container.LoggingClientFrom(dic.Get) - p.configuration = container.ConfigurationFrom(dic.Get) + var provider interfaces.SecretProvider - // attempt to create a new SecretProvider client only if security is enabled. - if p.IsSecurityEnabled() { + switch secret.IsSecurityEnabled() { + case true: + // attempt to create a new Secure client only if security is enabled. var err error - p.lc.Info("Creating SecretClient") + lc.Info("Creating SecretClient") - secretStoreConfig := p.configuration.GetBootstrap().SecretStore + secretStoreConfig := configuration.GetBootstrap().SecretStore for startupTimer.HasNotElapsed() { var secretConfig types.SecretConfig - p.lc.Info("Reading secret store configuration and authentication token") + lc.Info("Reading secret store configuration and authentication token") - secretConfig, err = p.getSecretConfig(secretStoreConfig, dic) + tokenLoader := container.AuthTokenLoaderFrom(dic.Get) + if tokenLoader == nil { + tokenLoader = authtokenloader.NewAuthTokenLoader(fileioperformer.NewDefaultFileIoPerformer()) + } + + secretConfig, err = getSecretConfig(secretStoreConfig, tokenLoader) if err == nil { + secureProvider := secret.NewSecureProvider(configuration, lc, tokenLoader) var secretClient secrets.SecretClient - p.lc.Info("Attempting to create secret client") - secretClient, err = secrets.NewClient(ctx, secretConfig, p.lc, p.defaultTokenExpiredCallback) + lc.Info("Attempting to create secret client") + secretClient, err = secrets.NewClient(ctx, secretConfig, lc, secureProvider.DefaultTokenExpiredCallback) if err == nil { - p.secretClient = secretClient - p.lc.Info("Created SecretClient") + secureProvider.SetClient(secretClient) + provider = secureProvider + lc.Info("Created SecretClient") break } } - p.lc.Warn(fmt.Sprintf("Retryable failure while creating SecretClient: %s", err.Error())) + lc.Warn(fmt.Sprintf("Retryable failure while creating SecretClient: %s", err.Error())) startupTimer.SleepForInterval() } if err != nil { - p.lc.Error(fmt.Sprintf("unable to create SecretClient: %s", err.Error())) + lc.Error(fmt.Sprintf("unable to create SecretClient: %s", err.Error())) return false } + + case false: + provider = secret.NewInsecureProvider(configuration, lc) } dic.Update(di.ServiceConstructorMap{ container.SecretProviderName: func(get di.Get) interface{} { - return p + return provider }, }) @@ -87,7 +101,7 @@ func (p *Provider) BootstrapHandler( // getSecretConfig creates a SecretConfig based on the SecretStoreInfo configuration properties. // If a token file is present it will override the Authentication.AuthToken value. -func (p *Provider) getSecretConfig(secretStoreInfo config.SecretStoreInfo, dic *di.Container) (types.SecretConfig, error) { +func getSecretConfig(secretStoreInfo config.SecretStoreInfo, tokenLoader authtokenloader.AuthTokenLoader) (types.SecretConfig, error) { secretConfig := types.SecretConfig{ Host: secretStoreInfo.Host, Port: secretStoreInfo.Port, @@ -101,25 +115,15 @@ func (p *Provider) getSecretConfig(secretStoreInfo config.SecretStoreInfo, dic * RetryWaitPeriod: secretStoreInfo.RetryWaitPeriod, } - if !p.IsSecurityEnabled() || secretStoreInfo.TokenFile == "" { + if !secret.IsSecurityEnabled() || secretStoreInfo.TokenFile == "" { return secretConfig, nil } - // only bother getting a token if security is enabled and the configuration-provided token file is not empty. - fileIoPerformer := container.FileIoPerformerFrom(dic.Get) - if fileIoPerformer == nil { - fileIoPerformer = fileioperformer.NewDefaultFileIoPerformer() - } - - tokenLoader := container.AuthTokenLoaderFrom(dic.Get) - if tokenLoader == nil { - tokenLoader = authtokenloader.NewAuthTokenLoader(fileIoPerformer) - } - token, err := tokenLoader.Load(secretStoreInfo.TokenFile) if err != nil { return secretConfig, err } + secretConfig.Authentication.AuthToken = token return secretConfig, nil } diff --git a/bootstrap/handlers/secret_test.go b/bootstrap/handlers/secret_test.go new file mode 100644 index 00000000..e645d3d7 --- /dev/null +++ b/bootstrap/handlers/secret_test.go @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright 2020 Intel Inc. + * + * 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 handlers + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strconv" + "sync" + "testing" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/secret" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/di" + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader/mocks" + "github.com/edgexfoundry/go-mod-secrets/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + expectedUsername = "admin" + expectedPassword = "password" + expectedPath = "/redisdb" +) + +var testTokenResponse = `{"auth":{"accessor":"9OvxnrjgV0JTYMeBreak7YJ9","client_token":"s.oPJ8uuJCkTRb2RDdcNova8wg","entity_id":"","lease_duration":3600,"metadata":{"edgex-service-name":"edgex-core-data"},"orphan":true,"policies":["default","edgex-service-edgex-core-data"],"renewable":true,"token_policies":["default","edgex-service-edgex-core-data"],"token_type":"service"},"data":null,"lease_duration":0,"lease_id":"","renewable":false,"request_id":"ee749ee1-c8bf-6fa9-3ed5-644181fc25b0","warnings":null,"wrap_info":null}` +var expectedSecrets = map[string]string{secret.UsernameKey: expectedUsername, secret.PasswordKey: expectedPassword} + +func TestProvider_BootstrapHandler(t *testing.T) { + tests := []struct { + Name string + Secure string + }{ + {"Valid Secure", "true"}, + {"Valid Insecure", "false"}, + } + + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + _ = os.Setenv(secret.EnvSecretStore, tc.Secure) + timer := startup.NewStartUpTimer("UnitTest") + + dic := di.NewContainer(di.ServiceConstructorMap{ + container.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.MockLogger{} + }, + container.ConfigurationInterfaceName: func(get di.Get) interface{} { + return TestConfig{} + }, + }) + + if tc.Secure == "true" { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/v1/auth/token/lookup-self": + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(testTokenResponse)) + case "/redisdb": + w.WriteHeader(http.StatusOK) + data := make(map[string]interface{}) + data["data"] = expectedSecrets + response, _ := json.Marshal(data) + _, _ = w.Write(response) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer testServer.Close() + + serverUrl, _ := url.Parse(testServer.URL) + port, _ := strconv.Atoi(serverUrl.Port()) + config := NewTestConfig(port) + + mockTokenLoader := &mocks.AuthTokenLoader{} + mockTokenLoader.On("Load", "token.json").Return("Test Token", nil) + dic.Update(di.ServiceConstructorMap{ + container.AuthTokenLoaderInterfaceName: func(get di.Get) interface{} { + return mockTokenLoader + }, + container.ConfigurationInterfaceName: func(get di.Get) interface{} { + return config + }, + }) + } + + actual := SecureProviderBootstrapHandler(context.Background(), &sync.WaitGroup{}, timer, dic) + require.True(t, actual) + actualProvider := container.SecretProviderFrom(dic.Get) + assert.NotNil(t, actualProvider) + + actualSecrets, err := actualProvider.GetSecrets(expectedPath) + require.NoError(t, err) + assert.Equal(t, expectedUsername, actualSecrets[secret.UsernameKey]) + assert.Equal(t, expectedPassword, actualSecrets[secret.PasswordKey]) + }) + } +} + +type TestConfig struct { + InsecureSecrets bootstrapConfig.InsecureSecrets + SecretStore bootstrapConfig.SecretStoreInfo +} + +func NewTestConfig(port int) TestConfig { + return TestConfig{ + SecretStore: bootstrapConfig.SecretStoreInfo{ + Host: "localhost", + Port: port, + Protocol: "http", + ServerName: "localhost", + TokenFile: "token.json", + Authentication: types.AuthenticationInfo{ + AuthType: "Dummy-Token", + AuthToken: "myToken", + }, + }, + } +} + +func (t TestConfig) UpdateFromRaw(_ interface{}) bool { + panic("implement me") +} + +func (t TestConfig) EmptyWritablePtr() interface{} { + panic("implement me") +} + +func (t TestConfig) UpdateWritableFromRaw(_ interface{}) bool { + panic("implement me") +} + +func (t TestConfig) GetBootstrap() bootstrapConfig.BootstrapConfiguration { + return bootstrapConfig.BootstrapConfiguration{ + SecretStore: t.SecretStore, + } +} + +func (t TestConfig) GetLogLevel() string { + panic("implement me") +} + +func (t TestConfig) GetRegistryInfo() bootstrapConfig.RegistryInfo { + panic("implement me") +} + +func (t TestConfig) GetInsecureSecrets() bootstrapConfig.InsecureSecrets { + return map[string]bootstrapConfig.InsecureSecretsInfo{ + "DB": { + Path: expectedPath, + Secrets: expectedSecrets, + }, + } +} diff --git a/bootstrap/interfaces/secret.go b/bootstrap/interfaces/secret.go index 0d3f2a4a..2143bcc1 100644 --- a/bootstrap/interfaces/secret.go +++ b/bootstrap/interfaces/secret.go @@ -16,7 +16,4 @@ type SecretProvider interface { // SecretsLastUpdated returns the last time secrets were updated SecretsLastUpdated() time.Time - - // IsSecurityEnabled return boolean indicating if running in secure mode or not - IsSecurityEnabled() bool } diff --git a/bootstrap/registration/registry_test.go b/bootstrap/registration/registry_test.go index e55cc9f3..99a93e91 100644 --- a/bootstrap/registration/registry_test.go +++ b/bootstrap/registration/registry_test.go @@ -140,6 +140,10 @@ type unitTestConfiguration struct { Registry config.RegistryInfo } +func (ut unitTestConfiguration) GetInsecureSecrets() config.InsecureSecrets { + return nil +} + func (ut unitTestConfiguration) UpdateFromRaw(rawConfig interface{}) bool { panic("should not be called") } diff --git a/bootstrap/secret/handler_test.go b/bootstrap/secret/handler_test.go deleted file mode 100644 index 56854e2f..00000000 --- a/bootstrap/secret/handler_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/******************************************************************************* - * Copyright 2020 Intel Inc. - * - * 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 secret - -import ( - "context" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "sync" - "testing" - - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" - "github.com/edgexfoundry/go-mod-bootstrap/di" - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var testTokenResponse = `{"auth":{"accessor":"9OvxnrjgV0JTYMeBydak7YJ9","client_token":"s.oPJ8uuJCkTRb2RDdcNvaz8wg","entity_id":"","lease_duration":3600,"metadata":{"edgex-service-name":"edgex-core-data"},"orphan":true,"policies":["default","edgex-service-edgex-core-data"],"renewable":true,"token_policies":["default","edgex-service-edgex-core-data"],"token_type":"service"},"data":null,"lease_duration":0,"lease_id":"","renewable":false,"request_id":"ee749ee1-c8bf-6fa9-3ed5-644181fc25b0","warnings":null,"wrap_info":null}` - -func TestProvider_BootstrapHandler(t *testing.T) { - timer := startup.NewStartUpTimer("UnitTest") - - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(testTokenResponse)) - })) - defer testServer.Close() - - url, _ := url.Parse(testServer.URL) - port, _ := strconv.Atoi(url.Port()) - config := NewTestConfig(port) - - mockTokenLoader := &mocks.AuthTokenLoader{} - mockTokenLoader.On("Load", "token.json").Return("Test Token", nil) - dic := di.NewContainer(di.ServiceConstructorMap{ - container.LoggingClientInterfaceName: func(get di.Get) interface{} { - return logger.NewClientStdOut("TestProvider_BootstrapHandler", false, "DEBUG") - }, - container.ConfigurationInterfaceName: func(get di.Get) interface{} { - return config - }, - container.AuthTokenLoaderInterfaceName: func(get di.Get) interface{} { - return mockTokenLoader - }, - }) - - target := NewProvider() - actual := target.BootstrapHandler(context.Background(), &sync.WaitGroup{}, timer, dic) - require.True(t, actual) - assert.NotNil(t, container.SecretProviderFrom(dic.Get)) -} diff --git a/bootstrap/secret/helper.go b/bootstrap/secret/helper.go new file mode 100644 index 00000000..bc1632ae --- /dev/null +++ b/bootstrap/secret/helper.go @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright 2020 Intel Inc. + * + * 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 secret + +import "os" + +const ( + EnvSecretStore = "EDGEX_SECURITY_SECRET_STORE" + UsernameKey = "username" + PasswordKey = "password" +) + +// IsSecurityEnabled determines if security has been enabled. +func IsSecurityEnabled() bool { + env := os.Getenv(EnvSecretStore) + return env != "false" // Any other value is considered secure mode enabled +} diff --git a/bootstrap/secret/insecure.go b/bootstrap/secret/insecure.go new file mode 100644 index 00000000..9da95a9c --- /dev/null +++ b/bootstrap/secret/insecure.go @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright 2020 Intel Inc. + * + * 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 secret + +import ( + "errors" + "fmt" + + "strings" + "time" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" +) + +// InsecureProvider implements the SecretProvider interface for insecure secrets +type InsecureProvider struct { + lc logger.LoggingClient + configuration interfaces.Configuration + lastUpdated time.Time +} + +// NewInsecureProvider creates, initializes Provider for insecure secrets. +func NewInsecureProvider(config interfaces.Configuration, lc logger.LoggingClient) *InsecureProvider { + return &InsecureProvider{ + configuration: config, + lc: lc, + lastUpdated: time.Now(), + } +} + +// GetSecrets retrieves secrets from a Insecure Secrets secret store. +// path specifies the type or location of the secrets to retrieve. +// keys specifies the secrets which to retrieve. If no keys are provided then all the keys associated with the +// specified path will be returned. +func (p *InsecureProvider) GetSecrets(path string, keys ...string) (map[string]string, error) { + results := make(map[string]string) + pathExists := false + var missingKeys []string + + insecureSecrets := p.configuration.GetInsecureSecrets() + if insecureSecrets == nil { + err := fmt.Errorf("InsecureSecrets missing from configuration") + return nil, err + } + + for _, insecureSecret := range insecureSecrets { + if insecureSecret.Path == path { + if len(keys) == 0 { + // If no keys are provided then all the keys associated with the specified path will be returned + for k, v := range insecureSecret.Secrets { + results[k] = v + } + return results, nil + } + + pathExists = true + for _, key := range keys { + value, keyExists := insecureSecret.Secrets[key] + if !keyExists { + missingKeys = append(missingKeys, key) + continue + } + results[key] = value + } + } + } + + if len(missingKeys) > 0 { + err := fmt.Errorf("No value for the keys: [%s] exists", strings.Join(missingKeys, ",")) + return nil, err + } + + if !pathExists { + // if path is not in secret store + err := fmt.Errorf("Error, path (%v) doesn't exist in secret store", path) + return nil, err + } + + return results, nil +} + +// StoreSecrets stores the secrets, but is not supported for Insecure Secrets +func (p *InsecureProvider) StoreSecrets(_ string, _ map[string]string) error { + return errors.New("storing secrets is not supported when running in insecure mode") +} + +// SecretsUpdated resets LastUpdate time for the Insecure Secrets. +func (p *InsecureProvider) SecretsUpdated() { + p.lastUpdated = time.Now() +} + +// SecretsLastUpdated returns the last time insecure secrets were updated +func (p *InsecureProvider) SecretsLastUpdated() time.Time { + return p.lastUpdated +} diff --git a/bootstrap/secret/insecure_test.go b/bootstrap/secret/insecure_test.go new file mode 100644 index 00000000..2d917a12 --- /dev/null +++ b/bootstrap/secret/insecure_test.go @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright 2020 Intel Inc. + * + * 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 secret + +import ( + "testing" + "time" + + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInsecureProvider_GetSecrets(t *testing.T) { + expected := map[string]string{"username": "admin", "password": "sam123!"} + + configAllSecrets := TestConfig{ + InsecureSecrets: map[string]bootstrapConfig.InsecureSecretsInfo{ + "DB": { + Path: "redis", + Secrets: expected, + }, + }, + } + + configMissingSecrets := TestConfig{ + InsecureSecrets: map[string]bootstrapConfig.InsecureSecretsInfo{ + "DB": { + Path: "redis", + }, + }, + } + + tests := []struct { + Name string + Path string + Keys []string + Config TestConfig + ExpectError bool + }{ + {"Valid", "redis", []string{"username", "password"}, configAllSecrets, false}, + {"Valid just path", "redis", nil, configAllSecrets, false}, + {"Invalid - No secrets", "redis", []string{"username", "password"}, configMissingSecrets, true}, + {"Invalid - Bad Path", "bogus", []string{"username", "password"}, configAllSecrets, true}, + } + + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + target := NewInsecureProvider(tc.Config, logger.MockLogger{}) + actual, err := target.GetSecrets(tc.Path, tc.Keys...) + if tc.ExpectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, expected, actual) + }) + } +} + +func TestInsecureProvider_StoreSecrets_Secure(t *testing.T) { + target := NewInsecureProvider(nil, nil) + err := target.StoreSecrets("myPath", map[string]string{"Key": "value"}) + require.Error(t, err) +} + +func TestInsecureProvider_SecretsUpdated_SecretsLastUpdated(t *testing.T) { + target := NewInsecureProvider(nil, logger.MockLogger{}) + previous := target.SecretsLastUpdated() + time.Sleep(1 * time.Second) + target.SecretsUpdated() + current := target.SecretsLastUpdated() + assert.True(t, current.After(previous)) +} diff --git a/bootstrap/secret/provider.go b/bootstrap/secret/secure.go similarity index 50% rename from bootstrap/secret/provider.go rename to bootstrap/secret/secure.go index 22313239..5899f5fb 100644 --- a/bootstrap/secret/provider.go +++ b/bootstrap/secret/secure.go @@ -22,59 +22,46 @@ import ( "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader" - "github.com/edgexfoundry/go-mod-secrets/pkg/token/fileioperformer" "github.com/edgexfoundry/go-mod-secrets/secrets" - "os" - "strings" "sync" "time" ) -const ( - EnvSecretStore = "EDGEX_SECURITY_SECRET_STORE" - UsernameKey = "username" - PasswordKey = "password" -) - // Provider implements the SecretProvider interface -type Provider struct { +type SecureProvider struct { secretClient secrets.SecretClient lc logger.LoggingClient + loader authtokenloader.AuthTokenLoader configuration interfaces.Configuration secretsCache map[string]map[string]string // secret's path, key, value cacheMutex *sync.Mutex lastUpdated time.Time } -// NewProvider creates, basic initializes and returns a new Provider instance. -// The full initialization occurs in the bootstrap handler. -func NewProvider() *Provider { - return &Provider{ - secretsCache: make(map[string]map[string]string), - cacheMutex: &sync.Mutex{}, - lastUpdated: time.Now(), +// NewSecureProvider creates & initializes Provider instance for secure secrets. +func NewSecureProvider(config interfaces.Configuration, lc logger.LoggingClient, loader authtokenloader.AuthTokenLoader) *SecureProvider { + provider := &SecureProvider{ + configuration: config, + lc: lc, + loader: loader, + secretsCache: make(map[string]map[string]string), + cacheMutex: &sync.Mutex{}, + lastUpdated: time.Now(), } + return provider } -// NewProviderWithDependents creates, initializes and returns a new full initialized Provider instance. -func NewProviderWithDependents(client secrets.SecretClient, config interfaces.Configuration, lc logger.LoggingClient) *Provider { - provider := NewProvider() - provider.secretClient = client - provider.configuration = config - provider.lc = lc - return provider +// SetClient sets the secret client that is used to access the secure secrets +func (p *SecureProvider) SetClient(client secrets.SecretClient) { + p.secretClient = client } // GetSecrets retrieves secrets from a secret store. // path specifies the type or location of the secrets to retrieve. // keys specifies the secrets which to retrieve. If no keys are provided then all the keys associated with the // specified path will be returned. -func (p *Provider) GetSecrets(path string, keys ...string) (map[string]string, error) { - if !p.IsSecurityEnabled() { - return p.getInsecureSecrets(path, keys...) - } - +func (p *SecureProvider) GetSecrets(path string, keys ...string) (map[string]string, error) { if cachedSecrets := p.getSecretsCache(path, keys...); cachedSecrets != nil { return cachedSecrets, nil } @@ -83,68 +70,17 @@ func (p *Provider) GetSecrets(path string, keys ...string) (map[string]string, e return nil, errors.New("can't get secret(p), secret client is not properly initialized") } - secrets, err := p.secretClient.GetSecrets(path, keys...) + secureSecrets, err := p.secretClient.GetSecrets(path, keys...) if err != nil { return nil, err } - p.updateSecretsCache(path, secrets) - return secrets, nil -} - -// GetInsecureSecrets retrieves secrets from the Writable.InsecureSecrets section of the configuration -// path specifies the type or location of the secrets to retrieve. -// keys specifies the secrets which to retrieve. If no keys are provided then all the keys associated with the -// specified path will be returned. -func (p *Provider) getInsecureSecrets(path string, keys ...string) (map[string]string, error) { - secrets := make(map[string]string) - pathExists := false - var missingKeys []string - - insecureSecrets := p.configuration.GetInsecureSecrets() - if insecureSecrets == nil { - err := fmt.Errorf("InsecureSecrets missing from configuration") - return nil, err - } - - for _, insecureSecret := range insecureSecrets { - if insecureSecret.Path == path { - if len(keys) == 0 { - // If no keys are provided then all the keys associated with the specified path will be returned - for k, v := range insecureSecret.Secrets { - secrets[k] = v - } - return secrets, nil - } - - pathExists = true - for _, key := range keys { - value, keyExists := insecureSecret.Secrets[key] - if !keyExists { - missingKeys = append(missingKeys, key) - continue - } - secrets[key] = value - } - } - } - - if len(missingKeys) > 0 { - err := fmt.Errorf("No value for the keys: [%s] exists", strings.Join(missingKeys, ",")) - return nil, err - } - - if !pathExists { - // if path is not in secret store - err := fmt.Errorf("Error, path (%v) doesn't exist in secret store", path) - return nil, err - } - - return secrets, nil + p.updateSecretsCache(path, secureSecrets) + return secureSecrets, nil } -func (p *Provider) getSecretsCache(path string, keys ...string) map[string]string { - secrets := make(map[string]string) +func (p *SecureProvider) getSecretsCache(path string, keys ...string) map[string]string { + secureSecrets := make(map[string]string) // Synchronize cache access p.cacheMutex.Lock() @@ -161,19 +97,19 @@ func (p *Provider) getSecretsCache(path string, keys ...string) map[string]strin if !allKeysExistInCache { return nil } - secrets[key] = value + secureSecrets[key] = value } - // return secrets if the requested keys exist in cache + // return secureSecrets if the requested keys exist in cache if allKeysExistInCache { - return secrets + return secureSecrets } } return nil } -func (p *Provider) updateSecretsCache(path string, secrets map[string]string) { +func (p *SecureProvider) updateSecretsCache(path string, secrets map[string]string) { // Synchronize cache access p.cacheMutex.Lock() defer p.cacheMutex.Unlock() @@ -191,11 +127,7 @@ func (p *Provider) updateSecretsCache(path string, secrets map[string]string) { // it sets the values requested at provided keys // path specifies the type or location of the secrets to store // secrets map specifies the "key": "value" pairs of secrets to store -func (p *Provider) StoreSecrets(path string, secrets map[string]string) error { - if !p.IsSecurityEnabled() { - return errors.New("storing secrets is not supported when running in insecure mode") - } - +func (p *SecureProvider) StoreSecrets(path string, secrets map[string]string) error { if p.secretClient == nil { return errors.New("can't store secret(p) 'SecretProvider' is not properly initialized") } @@ -215,35 +147,25 @@ func (p *Provider) StoreSecrets(path string, secrets map[string]string) error { return nil } -// InsecureSecretsUpdated resets LastUpdate if not running in secure mode. If running in secure mode, changes to -// InsecureSecrets have no impact and are not used. -func (p *Provider) InsecureSecretsUpdated() { - if !p.IsSecurityEnabled() { - p.lastUpdated = time.Now() - } +// SecretsUpdated is not need for secure secrets as this is handled when secrets are stored. +func (p *SecureProvider) SecretsUpdated() { + // Do nothing } -func (p *Provider) SecretsLastUpdated() time.Time { +// SecretsLastUpdated returns the last time secure secrets were updated +func (p *SecureProvider) SecretsLastUpdated() time.Time { return p.lastUpdated } -// isSecurityEnabled determines if security has been enabled. -func (p *Provider) IsSecurityEnabled() bool { - env := os.Getenv(EnvSecretStore) - return env != "false" // Any other value is considered secure mode enabled -} - // defaultTokenExpiredCallback is the default implementation of tokenExpiredCallback function // It utilizes the tokenFile to re-read the token and enable retry if any update from the expired token -func (p *Provider) defaultTokenExpiredCallback(expiredToken string) (replacementToken string, retry bool) { +func (p *SecureProvider) DefaultTokenExpiredCallback(expiredToken string) (replacementToken string, retry bool) { tokenFile := p.configuration.GetBootstrap().SecretStore.TokenFile // during the callback, we want to re-read the token from the disk // specified by tokenFile and set the retry to true if a new token // is different from the expiredToken - fileIoPerformer := fileioperformer.NewDefaultFileIoPerformer() - authTokenLoader := authtokenloader.NewAuthTokenLoader(fileIoPerformer) - reReadToken, err := authTokenLoader.Load(tokenFile) + reReadToken, err := p.loader.Load(tokenFile) if err != nil { p.lc.Error(fmt.Sprintf("fail to load auth token from tokenFile %s: %v", tokenFile, err)) return "", false diff --git a/bootstrap/secret/provider_test.go b/bootstrap/secret/secure_test.go similarity index 67% rename from bootstrap/secret/provider_test.go rename to bootstrap/secret/secure_test.go index 9caa3461..47952063 100644 --- a/bootstrap/secret/provider_test.go +++ b/bootstrap/secret/secure_test.go @@ -16,21 +16,20 @@ package secret import ( "errors" - "os" "testing" "time" bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/config" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" "github.com/edgexfoundry/go-mod-secrets/pkg" - "github.com/edgexfoundry/go-mod-secrets/pkg/types" + mocks2 "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader/mocks" "github.com/edgexfoundry/go-mod-secrets/secrets" "github.com/edgexfoundry/go-mod-secrets/secrets/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestProvider_GetSecrets(t *testing.T) { +func TestSecureProvider_GetSecrets(t *testing.T) { expected := map[string]string{"username": "admin", "password": "sam123!"} mock := &mocks.SecretClient{} @@ -39,45 +38,23 @@ func TestProvider_GetSecrets(t *testing.T) { notfound := []string{"username", "password"} mock.On("GetSecrets", "missing", "username", "password").Return(nil, pkg.NewErrSecretsNotFound(notfound)) - configAllSecrets := TestConfig{ - InsecureSecrets: map[string]bootstrapConfig.InsecureSecretsInfo{ - "DB": { - Path: "redis", - Secrets: expected, - }, - }, - } - - configMissingSecrets := TestConfig{ - InsecureSecrets: map[string]bootstrapConfig.InsecureSecretsInfo{ - "DB": { - Path: "redis", - }, - }, - } - tests := []struct { Name string - Secure string Path string Keys []string Config TestConfig Client secrets.SecretClient ExpectError bool }{ - {"Valid Secure", "true", "redis", []string{"username", "password"}, TestConfig{}, mock, false}, - {"Invalid Secure", "true", "missing", []string{"username", "password"}, TestConfig{}, mock, true}, - {"Invalid No Client", "true", "redis", []string{"username", "password"}, TestConfig{}, nil, true}, - {"Valid Insecure", "false", "redis", []string{"username", "password"}, configAllSecrets, mock, false}, - {"Valid Insecure just path", "false", "redis", nil, configAllSecrets, mock, false}, - {"Invalid Insecure - No secrets", "false", "redis", []string{"username", "password"}, configMissingSecrets, mock, true}, - {"Invalid Insecure - Bad Path", "false", "bogus", []string{"username", "password"}, configAllSecrets, mock, true}, + {"Valid Secure", "redis", []string{"username", "password"}, TestConfig{}, mock, false}, + {"Invalid Secure", "missing", []string{"username", "password"}, TestConfig{}, mock, true}, + {"Invalid No Client", "redis", []string{"username", "password"}, TestConfig{}, nil, true}, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { - os.Setenv(EnvSecretStore, tc.Secure) - target := NewProviderWithDependents(tc.Client, tc.Config, logger.MockLogger{}) + target := NewSecureProvider(tc.Config, logger.MockLogger{}, nil) + target.SetClient(tc.Client) actual, err := target.GetSecrets(tc.Path, tc.Keys...) if tc.ExpectError { require.Error(t, err) @@ -90,15 +67,16 @@ func TestProvider_GetSecrets(t *testing.T) { } } -func TestProvider_GetSecrets_SecureCached(t *testing.T) { - os.Setenv(EnvSecretStore, "true") +func TestSecureProvider_GetSecrets_Cached(t *testing.T) { expected := map[string]string{"username": "admin", "password": "sam123!"} mock := &mocks.SecretClient{} // Use the Once method so GetSecrets can be changed below mock.On("GetSecrets", "redis", "username", "password").Return(expected, nil).Once() - target := NewProviderWithDependents(mock, nil, logger.MockLogger{}) + target := NewSecureProvider(nil, logger.MockLogger{}, nil) + target.SetClient(mock) + actual, err := target.GetSecrets("redis", "username", "password") require.NoError(t, err) assert.Equal(t, expected, actual) @@ -115,8 +93,7 @@ func TestProvider_GetSecrets_SecureCached(t *testing.T) { require.Error(t, err) } -func TestProvider_GetSecrets_SecureCached_Invalidated(t *testing.T) { - os.Setenv(EnvSecretStore, "true") +func TestSecureProvider_GetSecrets_Cached_Invalidated(t *testing.T) { expected := map[string]string{"username": "admin", "password": "sam123!"} mock := &mocks.SecretClient{} @@ -124,7 +101,9 @@ func TestProvider_GetSecrets_SecureCached_Invalidated(t *testing.T) { mock.On("GetSecrets", "redis", "username", "password").Return(expected, nil).Once() mock.On("StoreSecrets", "redis", expected).Return(nil) - target := NewProviderWithDependents(mock, nil, logger.MockLogger{}) + target := NewSecureProvider(nil, logger.MockLogger{}, nil) + target.SetClient(mock) + actual, err := target.GetSecrets("redis", "username", "password") require.NoError(t, err) assert.Equal(t, expected, actual) @@ -139,7 +118,7 @@ func TestProvider_GetSecrets_SecureCached_Invalidated(t *testing.T) { require.Error(t, err) } -func TestProvider_StoreSecrets_Secure(t *testing.T) { +func TestSecureProvider_StoreSecrets_Secure(t *testing.T) { input := map[string]string{"username": "admin", "password": "sam123!"} mock := &mocks.SecretClient{} mock.On("StoreSecrets", "redis", input).Return(nil) @@ -155,13 +134,13 @@ func TestProvider_StoreSecrets_Secure(t *testing.T) { {"Valid Secure", "true", "redis", mock, false}, {"Invalid no client", "true", "redis", nil, true}, {"Invalid internal error", "true", "error", mock, true}, - {"Invalid Non-secure", "false", "redis", mock, true}, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { - os.Setenv(EnvSecretStore, tc.Secure) - target := NewProviderWithDependents(tc.Client, nil, logger.MockLogger{}) + target := NewSecureProvider(nil, logger.MockLogger{}, nil) + target.SetClient(tc.Client) + err := target.StoreSecrets(tc.Path, input) if tc.ExpectError { require.Error(t, err) @@ -173,14 +152,13 @@ func TestProvider_StoreSecrets_Secure(t *testing.T) { } } -func TestProvider_SecretsLastUpdated(t *testing.T) { - os.Setenv(EnvSecretStore, "true") - +func TestSecureProvider_SecretsLastUpdated(t *testing.T) { input := map[string]string{"username": "admin", "password": "sam123!"} mock := &mocks.SecretClient{} mock.On("StoreSecrets", "redis", input).Return(nil) - target := NewProviderWithDependents(mock, nil, logger.MockLogger{}) + target := NewSecureProvider(nil, logger.MockLogger{}, nil) + target.SetClient(mock) previous := target.SecretsLastUpdated() time.Sleep(1 * time.Second) @@ -190,14 +168,54 @@ func TestProvider_SecretsLastUpdated(t *testing.T) { assert.True(t, current.After(previous)) } -func TestProvider_InsecureSecretsUpdated(t *testing.T) { - os.Setenv(EnvSecretStore, "false") - target := NewProviderWithDependents(nil, nil, logger.MockLogger{}) +func TestSecureProvider_SecretsUpdated(t *testing.T) { + target := NewSecureProvider(nil, logger.MockLogger{}, nil) previous := target.SecretsLastUpdated() time.Sleep(1 * time.Second) - target.InsecureSecretsUpdated() + target.SecretsUpdated() current := target.SecretsLastUpdated() - assert.True(t, current.After(previous)) + // Since the SecureProvider does nothing for SecretsUpdated, LastUpdated shouldn't change + assert.Equal(t, previous, current) +} + +func TestSecureProvider_DefaultTokenExpiredCallback(t *testing.T) { + goodTokenFile := "good-token.json" + badTokenFile := "bad-token.json" + sameTokenFile := "same-token.json" + newToken := "new token" + expiredToken := "expired token" + + mockTokenLoader := &mocks2.AuthTokenLoader{} + mockTokenLoader.On("Load", goodTokenFile).Return(newToken, nil) + mockTokenLoader.On("Load", sameTokenFile).Return(expiredToken, nil) + mockTokenLoader.On("Load", badTokenFile).Return("", errors.New("Not Found")) + + tests := []struct { + Name string + TokenFile string + ExpiredToken string + ExpectedToken string + ExpectedRetry bool + }{ + {"Valid", goodTokenFile, expiredToken, "new token", true}, + {"Bad File", badTokenFile, "", "", false}, + {"Same Token", sameTokenFile, expiredToken, expiredToken, false}, + } + + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + config := TestConfig{ + SecretStore: bootstrapConfig.SecretStoreInfo{ + TokenFile: tc.TokenFile, + }, + } + + target := NewSecureProvider(config, logger.MockLogger{}, mockTokenLoader) + actualToken, actualRetry := target.DefaultTokenExpiredCallback(tc.ExpiredToken) + assert.Equal(t, tc.ExpectedToken, actualToken) + assert.Equal(t, tc.ExpectedRetry, actualRetry) + }) + } } type TestConfig struct { @@ -205,23 +223,7 @@ type TestConfig struct { SecretStore bootstrapConfig.SecretStoreInfo } -func NewTestConfig(port int) TestConfig { - return TestConfig{ - SecretStore: bootstrapConfig.SecretStoreInfo{ - Host: "localhost", - Port: port, - Protocol: "http", - ServerName: "localhost", - TokenFile: "token.json", - Authentication: types.AuthenticationInfo{ - AuthType: "Dummy-Token", - AuthToken: "myToken", - }, - }, - } -} - -func (t TestConfig) UpdateFromRaw(rawConfig interface{}) bool { +func (t TestConfig) UpdateFromRaw(_ interface{}) bool { panic("implement me") } @@ -229,7 +231,7 @@ func (t TestConfig) EmptyWritablePtr() interface{} { panic("implement me") } -func (t TestConfig) UpdateWritableFromRaw(rawWritable interface{}) bool { +func (t TestConfig) UpdateWritableFromRaw(_ interface{}) bool { panic("implement me") }