From 4a01cf347182975ebca7b3c4df598ac3206ea417 Mon Sep 17 00:00:00 2001 From: lenny Date: Wed, 18 Nov 2020 15:14:53 -0700 Subject: [PATCH 1/5] refactor: Secret Provider for all services Refactored Secret Provider per ADR so it can be used by all services (core, support, App & Device) closes #126 Signed-off-by: lenny --- bootstrap/bootstrap.go | 2 +- bootstrap/config/config.go | 29 +- bootstrap/container/certificate.go | 29 -- bootstrap/container/credentials.go | 29 -- bootstrap/container/logging.go | 10 +- bootstrap/container/secret.go | 24 +- bootstrap/container/token.go | 45 +++ bootstrap/handlers/secret/certkeypair.go | 30 -- .../handlers/secret/client/mockserver_test.go | 81 ------ .../secret/client/testdata/replacement.json | 9 - .../secret/client/testdata/testToken.json | 9 - .../handlers/secret/client/vaultclient.go | 87 ------ .../secret/client/vaultclient_test.go | 253 ----------------- bootstrap/handlers/secret/credentials.go | 40 --- bootstrap/handlers/secret/secret.go | 140 ---------- bootstrap/interfaces/configuration.go | 3 + bootstrap/interfaces/mocks/SecretProvider.go | 91 ++++++ bootstrap/interfaces/secret.go | 21 ++ bootstrap/secret/handler.go | 125 +++++++++ bootstrap/secret/handler_test.go | 68 +++++ bootstrap/secret/provider.go | 258 ++++++++++++++++++ bootstrap/secret/provider_test.go | 252 +++++++++++++++++ config/types.go | 26 +- go.mod | 4 +- 24 files changed, 935 insertions(+), 730 deletions(-) delete mode 100644 bootstrap/container/certificate.go delete mode 100644 bootstrap/container/credentials.go create mode 100644 bootstrap/container/token.go delete mode 100644 bootstrap/handlers/secret/certkeypair.go delete mode 100644 bootstrap/handlers/secret/client/mockserver_test.go delete mode 100644 bootstrap/handlers/secret/client/testdata/replacement.json delete mode 100644 bootstrap/handlers/secret/client/testdata/testToken.json delete mode 100644 bootstrap/handlers/secret/client/vaultclient.go delete mode 100644 bootstrap/handlers/secret/client/vaultclient_test.go delete mode 100644 bootstrap/handlers/secret/credentials.go delete mode 100644 bootstrap/handlers/secret/secret.go create mode 100644 bootstrap/interfaces/mocks/SecretProvider.go create mode 100644 bootstrap/interfaces/secret.go create mode 100644 bootstrap/secret/handler.go create mode 100644 bootstrap/secret/handler_test.go create mode 100644 bootstrap/secret/provider.go create mode 100644 bootstrap/secret/provider_test.go diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 4575f9d3..e08be7e2 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -101,7 +101,7 @@ func RunAndReturnWaitGroup( envVars := environment.NewVariables() - configProcessor := config.NewProcessor(lc, commonFlags, envVars, startupTimer, ctx, &wg, configUpdated) + configProcessor := config.NewProcessor(lc, commonFlags, envVars, startupTimer, ctx, &wg, configUpdated, dic) if err := configProcessor.Process(serviceKey, configStem, serviceConfig); err != nil { fatalError(err, lc) } diff --git a/bootstrap/config/config.go b/bootstrap/config/config.go index c2995cd7..a5c10573 100644 --- a/bootstrap/config/config.go +++ b/bootstrap/config/config.go @@ -24,6 +24,8 @@ import ( "sync" "github.com/BurntSushi/toml" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/di" "github.com/edgexfoundry/go-mod-configuration/configuration" configTypes "github.com/edgexfoundry/go-mod-configuration/pkg/types" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" @@ -48,6 +50,7 @@ type Processor struct { ctx context.Context wg *sync.WaitGroup configUpdated UpdatedStream + dic *di.Container overwriteConfig bool } @@ -60,6 +63,7 @@ func NewProcessor( ctx context.Context, wg *sync.WaitGroup, configUpdated UpdatedStream, + dic *di.Container, ) *Processor { return &Processor{ Logger: lc, @@ -69,6 +73,7 @@ func NewProcessor( ctx: ctx, wg: wg, configUpdated: configUpdated, + dic: dic, } } @@ -283,13 +288,35 @@ func (cp *Processor) listenForChanges(serviceConfig interfaces.Configuration, co continue } + previousInsecureSecrets := serviceConfig.GetInsecureSecrets() + previousLogLevel := serviceConfig.GetLogLevel() + if !serviceConfig.UpdateWritableFromRaw(raw) { lc.Error("ListenForChanges() type check failed") return } + currentInsecureSecrets := serviceConfig.GetInsecureSecrets() + currentLogLevel := serviceConfig.GetLogLevel() + lc.Info("Writeable configuration has been updated from the Configuration Provider") - _ = lc.SetLogLevel(serviceConfig.GetLogLevel()) + + // Note: Updates occur one setting at a time so only have to look for single changes + switch { + case currentLogLevel != previousLogLevel: + _ = lc.SetLogLevel(serviceConfig.GetLogLevel()) + lc.Info(fmt.Sprintf("Logging level changed to %s", currentLogLevel)) + + // InsecureSecrets (map) will be nil if not in the original TOML used to seed the Config Provider, + // so ignore it if this is the case. + case currentInsecureSecrets != nil && + !reflect.DeepEqual(currentInsecureSecrets, previousInsecureSecrets): + lc.Info("Insecure Secrets have been updated") + secretProvider := container.SecretProviderFrom(cp.dic.Get) + if secretProvider != nil { + secretProvider.InsecureSecretsUpdated() + } + } if cp.configUpdated != nil { cp.configUpdated <- struct{}{} diff --git a/bootstrap/container/certificate.go b/bootstrap/container/certificate.go deleted file mode 100644 index 8f62ea77..00000000 --- a/bootstrap/container/certificate.go +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************** - * Copyright 2020 Dell 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 container - -import ( - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/di" -) - -// CertificateProviderName contains the name of the interfaces.CertificateProvider implementation in the DIC. -var CertificateProviderName = di.TypeInstanceToName((*interfaces.CertificateProvider)(nil)) - -// CertificateProviderFrom helper function queries the DIC and returns the interfaces.CertificateProviderName -// implementation. -func CertificateProviderFrom(get di.Get) interfaces.CertificateProvider { - return get(CertificateProviderName).(interfaces.CertificateProvider) -} diff --git a/bootstrap/container/credentials.go b/bootstrap/container/credentials.go deleted file mode 100644 index 31f248a3..00000000 --- a/bootstrap/container/credentials.go +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************** - * Copyright 2019 Dell 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 container - -import ( - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/di" -) - -// CredentialsProviderName contains the name of the interfaces.CredentialsProvider implementation in the DIC. -var CredentialsProviderName = di.TypeInstanceToName((*interfaces.CredentialsProvider)(nil)) - -// CredentialsProviderFrom helper function queries the DIC and returns the interfaces.CredentialsProviderName -// implementation. -func CredentialsProviderFrom(get di.Get) interfaces.CredentialsProvider { - return get(CredentialsProviderName).(interfaces.CredentialsProvider) -} diff --git a/bootstrap/container/logging.go b/bootstrap/container/logging.go index ce7eda5d..4641c880 100644 --- a/bootstrap/container/logging.go +++ b/bootstrap/container/logging.go @@ -16,7 +16,6 @@ package container import ( "github.com/edgexfoundry/go-mod-bootstrap/di" - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" ) @@ -25,9 +24,10 @@ var LoggingClientInterfaceName = di.TypeInstanceToName((*logger.LoggingClient)(n // LoggingClientFrom helper function queries the DIC and returns the logger.loggingClient implementation. func LoggingClientFrom(get di.Get) logger.LoggingClient { - if loggingClient, ok := get(LoggingClientInterfaceName).(logger.LoggingClient); ok { - return loggingClient - } else { - return nil + loggingClient := get(LoggingClientInterfaceName) + if loggingClient != nil { + return loggingClient.(logger.LoggingClient) } + + return (logger.LoggingClient)(nil) } diff --git a/bootstrap/container/secret.go b/bootstrap/container/secret.go index bbff7617..ab155d69 100644 --- a/bootstrap/container/secret.go +++ b/bootstrap/container/secret.go @@ -1,5 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. + * Copyright 2020 Intel Corp. * * 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 @@ -15,15 +16,28 @@ package container import ( + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" "github.com/edgexfoundry/go-mod-bootstrap/di" - - "github.com/edgexfoundry/go-mod-secrets/pkg" + "github.com/edgexfoundry/go-mod-secrets/secrets" ) // SecretClientName contains the name of the registry.Client implementation in the DIC. -var SecretClientName = di.TypeInstanceToName((*pkg.SecretClient)(nil)) +var SecretClientName = di.TypeInstanceToName((*secrets.SecretClient)(nil)) // SecretClientFrom helper function queries the DIC and returns the pkg.SecretClient implementation. -func SecretClientFrom(get di.Get) pkg.SecretClient { - return get(SecretClientName).(pkg.SecretClient) +func SecretClientFrom(get di.Get) secrets.SecretClient { + return get(SecretClientName).(secrets.SecretClient) +} + +// SecretProviderName contains the name of the interfaces.SecretProvider implementation in the DIC. +var SecretProviderName = di.TypeInstanceToName((*interfaces.SecretProvider)(nil)) + +// CredentialsProviderFrom helper function queries the DIC and returns the interfaces.CredentialsProviderName +// implementation. +func SecretProviderFrom(get di.Get) interfaces.SecretProvider { + client := get(SecretProviderName).(interfaces.SecretProvider) + if client != nil { + return client.(interfaces.SecretProvider) + } + return (interfaces.SecretProvider)(nil) } diff --git a/bootstrap/container/token.go b/bootstrap/container/token.go new file mode 100644 index 00000000..16ab0682 --- /dev/null +++ b/bootstrap/container/token.go @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright 2020 Intel Corp. + * + * 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 container + +import ( + "github.com/edgexfoundry/go-mod-bootstrap/di" + "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader" + "github.com/edgexfoundry/go-mod-secrets/pkg/token/fileioperformer" +) + +// FileIoPerformerInterfaceName contains the name of the fileioperformer.FileIoPerformer implementation in the DIC. +var FileIoPerformerInterfaceName = di.TypeInstanceToName((*fileioperformer.FileIoPerformer)(nil)) + +// FileIoPerformerFrom helper function queries the DIC and returns the fileioperformer.FileIoPerformer implementation. +func FileIoPerformerFrom(get di.Get) fileioperformer.FileIoPerformer { + fileIo := get(FileIoPerformerInterfaceName) + if fileIo != nil { + return fileIo.(fileioperformer.FileIoPerformer) + } + return (fileioperformer.FileIoPerformer)(nil) +} + +// FileIoPerformerInterfaceName contains the name of the fileioperformer.FileIoPerformer implementation in the DIC. +var AuthTokenLoaderInterfaceName = di.TypeInstanceToName((*authtokenloader.AuthTokenLoader)(nil)) + +// FileIoPerformerFrom helper function queries the DIC and returns the fileioperformer.FileIoPerformer implementation. +func AuthTokenLoaderFrom(get di.Get) authtokenloader.AuthTokenLoader { + loader := get(AuthTokenLoaderInterfaceName) + if loader != nil { + return loader.(authtokenloader.AuthTokenLoader) + } + return (authtokenloader.AuthTokenLoader)(nil) +} diff --git a/bootstrap/handlers/secret/certkeypair.go b/bootstrap/handlers/secret/certkeypair.go deleted file mode 100644 index 146ba706..00000000 --- a/bootstrap/handlers/secret/certkeypair.go +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************** - * Copyright 2020 Dell 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 "github.com/edgexfoundry/go-mod-bootstrap/config" - -func (s *SecretProvider) GetCertificateKeyPair(path string) (config.CertKeyPair, error) { - secrets, err := s.secretClient.GetSecrets(path, "cert", "key") - if err != nil { - return config.CertKeyPair{}, err - } - - return config.CertKeyPair{ - Cert: secrets["cert"], - Key: secrets["key"], - }, nil - -} diff --git a/bootstrap/handlers/secret/client/mockserver_test.go b/bootstrap/handlers/secret/client/mockserver_test.go deleted file mode 100644 index ae014f86..00000000 --- a/bootstrap/handlers/secret/client/mockserver_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright (c) 2019 Intel Corporation -// -// 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. -// -// SPDX-License-Identifier: Apache-2.0 -// - -package client - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "sync" - - "github.com/edgexfoundry/go-mod-secrets/pkg/providers/vault" -) - -// getMockTokenServer is a test helper for unit tests of vaultclient -func getMockTokenServer(tokenDataMap *sync.Map) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - urlPath := req.URL.String() - if req.Method == http.MethodGet && urlPath == "/v1/auth/token/lookup-self" { - token := req.Header.Get(vault.AuthTypeHeader) - sampleTokenLookup, exists := tokenDataMap.Load(token) - if !exists { - rw.WriteHeader(403) - _, _ = rw.Write([]byte("permission denied")) - } else { - resp := &vault.TokenLookupResponse{ - Data: sampleTokenLookup.(vault.TokenLookupMetadata), - } - if ret, err := json.Marshal(resp); err != nil { - rw.WriteHeader(500) - _, _ = rw.Write([]byte(err.Error())) - } else { - rw.WriteHeader(200) - _, _ = rw.Write(ret) - } - } - } else if req.Method == http.MethodPost && urlPath == "/v1/auth/token/renew-self" { - token := req.Header.Get(vault.AuthTypeHeader) - sampleTokenLookup, exists := tokenDataMap.Load(token) - if !exists { - rw.WriteHeader(403) - _, _ = rw.Write([]byte("permission denied")) - } else { - currentTTL := sampleTokenLookup.(vault.TokenLookupMetadata).Ttl - if currentTTL <= 0 { - // already expired - rw.WriteHeader(403) - _, _ = rw.Write([]byte("permission denied")) - } else { - tokenPeriod := sampleTokenLookup.(vault.TokenLookupMetadata).Period - - tokenDataMap.Store(token, vault.TokenLookupMetadata{ - Renewable: true, - Ttl: tokenPeriod, - Period: tokenPeriod, - }) - rw.WriteHeader(200) - _, _ = rw.Write([]byte("token renewed")) - } - } - } else { - rw.WriteHeader(404) - _, _ = rw.Write([]byte(fmt.Sprintf("Unknown urlPath: %s", urlPath))) - } - })) - return server -} diff --git a/bootstrap/handlers/secret/client/testdata/replacement.json b/bootstrap/handlers/secret/client/testdata/replacement.json deleted file mode 100644 index dbe73832..00000000 --- a/bootstrap/handlers/secret/client/testdata/replacement.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "keys": [ - "test-keys" - ], - "keys_base64": [ - "test-keys-base64" - ], - "root_token": "replacement-token" -} \ No newline at end of file diff --git a/bootstrap/handlers/secret/client/testdata/testToken.json b/bootstrap/handlers/secret/client/testdata/testToken.json deleted file mode 100644 index 20aaa9a9..00000000 --- a/bootstrap/handlers/secret/client/testdata/testToken.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "keys": [ - "test-keys" - ], - "keys_base64": [ - "test-keys-base64" - ], - "root_token": "test-token" -} \ No newline at end of file diff --git a/bootstrap/handlers/secret/client/vaultclient.go b/bootstrap/handlers/secret/client/vaultclient.go deleted file mode 100644 index 9f668cc0..00000000 --- a/bootstrap/handlers/secret/client/vaultclient.go +++ /dev/null @@ -1,87 +0,0 @@ -// -// Copyright (c) 2019 Intel Corporation -// -// 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. -// -// SPDX-License-Identifier: Apache-2.0 -// - -package client - -import ( - "context" - "fmt" - - "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/providers/vault" - "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader" - "github.com/edgexfoundry/go-mod-secrets/pkg/token/fileioperformer" -) - -// Vault is the structure to get the secret client from go-mod-secrets vault package -type Vault struct { - ctx context.Context - config vault.SecretConfig - lc logger.LoggingClient -} - -// NewVault is the constructor for Vault in order to get the Vault secret client -func NewVault(ctx context.Context, config vault.SecretConfig, lc logger.LoggingClient) Vault { - return Vault{ - ctx: ctx, - config: config, - lc: lc, - } -} - -// Get is the getter for Vault secret client from go-mod-secrets -func (c Vault) Get(secretStoreInfo config.SecretStoreInfo) (pkg.SecretClient, error) { - return vault.NewSecretClientFactory().NewSecretClient( - c.ctx, - c.config, - c.lc, - c.getDefaultTokenExpiredCallback(secretStoreInfo)) -} - -// getDefaultTokenExpiredCallback 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 (c Vault) getDefaultTokenExpiredCallback( - secretStoreInfo config.SecretStoreInfo) func(expiredToken string) (replacementToken string, retry bool) { - // if there is no tokenFile, then no replacement token can be used and hence no callback - if secretStoreInfo.TokenFile == "" { - return nil - } - - tokenFile := secretStoreInfo.TokenFile - return func(expiredToken string) (replacementToken string, retry bool) { - // 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) - if err != nil { - c.lc.Error(fmt.Sprintf("fail to load auth token from tokenFile %s: %v", tokenFile, err)) - return "", false - } - - if reReadToken == expiredToken { - c.lc.Error("No new replacement token found for the expired token") - return reReadToken, false - } - - return reReadToken, true - } -} diff --git a/bootstrap/handlers/secret/client/vaultclient_test.go b/bootstrap/handlers/secret/client/vaultclient_test.go deleted file mode 100644 index b4c10a42..00000000 --- a/bootstrap/handlers/secret/client/vaultclient_test.go +++ /dev/null @@ -1,253 +0,0 @@ -// -// Copyright (c) 2019 Intel Corporation -// -// 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. -// -// SPDX-License-Identifier: Apache-2.0 -// - -package client - -import ( - "context" - "fmt" - "net" - "net/url" - "strconv" - "sync" - "testing" - "time" - - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/logging" - "github.com/edgexfoundry/go-mod-bootstrap/config" - - "github.com/edgexfoundry/go-mod-secrets/pkg/providers/vault" -) - -func TestGetVaultClient(t *testing.T) { - // setup - tokenPeriod := 6 - tokenDataMap := initTokenData(tokenPeriod) - server := getMockTokenServer(tokenDataMap) - defer server.Close() - - serverURL, err := url.Parse(server.URL) - if err != nil { - t.Errorf("error on parsing server url %s: %s", server.URL, err) - } - host, port, _ := net.SplitHostPort(serverURL.Host) - portNum, _ := strconv.Atoi(port) - - bkgCtx := context.Background() - lc := logging.FactoryToStdout("clientTest") - - testSecretStoreInfo := config.SecretStoreInfo{ - Host: host, - Port: portNum, - Path: "/test", - Protocol: "http", - ServerName: "mockVaultServer", - } - - tests := []struct { - name string - authToken string - tokenFile string - expectedNewToken string - expectedNilCallback bool - expectedRetry bool - expectError bool - }{ - { - name: "New secret client with testToken1, more than half of TTL remaining", - authToken: "testToken1", - tokenFile: "testdata/replacement.json", - expectedNilCallback: false, - expectedNewToken: "replacement-token", - expectedRetry: true, - expectError: false, - }, - { - name: "New secret client with the same first token again", - authToken: "testToken1", - tokenFile: "testdata/replacement.json", - expectedNilCallback: false, - expectedNewToken: "replacement-token", - expectedRetry: true, - expectError: false, - }, - { - name: "New secret client with testToken2, half of TTL remaining", - authToken: "testToken2", - expectedNilCallback: false, - tokenFile: "testdata/replacement.json", - expectedNewToken: "replacement-token", - expectedRetry: true, - expectError: false, - }, - { - name: "New secret client with testToken3, less than half of TTL remaining", - authToken: "testToken3", - tokenFile: "testdata/replacement.json", - expectedNilCallback: false, - expectedNewToken: "replacement-token", - expectedRetry: true, - expectError: false, - }, - { - name: "New secret client with expired token, no TTL remaining", - authToken: "expiredToken", - tokenFile: "testdata/replacement.json", - expectedNilCallback: false, - expectedNewToken: "replacement-token", - expectedRetry: true, - expectError: true, - }, - { - name: "New secret client with expired token, non-existing TokenFile path", - authToken: "expiredToken", - tokenFile: "testdata/non-existing.json", - expectedNilCallback: false, - expectedNewToken: "", - expectedRetry: false, - expectError: true, - }, - { - name: "New secret client with expired test token, but same expired replacement token", - authToken: "test-token", - tokenFile: "testdata/testToken.json", - expectedNilCallback: false, - expectedNewToken: "test-token", - expectedRetry: false, - expectError: true, - }, - { - name: "New secret client with unauthenticated token", - authToken: "test-token", - expectedNilCallback: true, - expectedNewToken: "", - expectedRetry: false, - expectError: true, - }, - { - name: "New secret client with unrenewable token", - authToken: "unrenewableToken", - expectedNilCallback: true, - expectedNewToken: "", - expectedRetry: true, - expectError: false, - }, - { - name: "New secret client with no TokenFile", - authToken: "testToken1", - tokenFile: "", - expectedNilCallback: true, - expectedNewToken: "", - expectedRetry: false, - expectError: false, - }, - } - - for _, test := range tests { - testSecretStoreInfo.TokenFile = test.tokenFile - // pinned local variables to avoid scopelint warnings - testToken := test.authToken - cfgHTTP := vault.SecretConfig{ - Host: host, - Port: portNum, - Protocol: "http", - Authentication: vault.AuthenticationInfo{AuthToken: testToken}, - } - expectError := test.expectError - expectedCallbackNil := test.expectedNilCallback - expectedNewToken := test.expectedNewToken - expectedRetry := test.expectedRetry - - t.Run(test.name, func(t *testing.T) { - sclient := NewVault(bkgCtx, cfgHTTP, lc) - _, err := sclient.Get(testSecretStoreInfo) - - if expectError && err == nil { - t.Error("Expected error but none was received") - } - - if !expectError && err != nil { - t.Errorf("Unexpected error: %s", err.Error()) - } - - tokenCallback := sclient.getDefaultTokenExpiredCallback(testSecretStoreInfo) - if expectedCallbackNil && tokenCallback != nil { - t.Error("expected nil token expired callback func, but found not nil") - } - if !expectedCallbackNil { - if tokenCallback == nil { - t.Error("expected some non-nil token expired callback func, but found nil") - } else { - repToken, retry := tokenCallback(testToken) - - if repToken != expectedNewToken { - t.Errorf("expected a new token [%s] from callback but got [%s]", expectedNewToken, repToken) - } - - if retry != expectedRetry { - t.Errorf("expected retry %v for a default token expired callback but got %v", expectedRetry, retry) - } - - lc.Debug(fmt.Sprintf("repToken = %s, retry = %v", repToken, retry)) - } - } - }) - } - // wait for some time to allow renewToken to be run if any - time.Sleep(7 * time.Second) -} - -func initTokenData(tokenPeriod int) *sync.Map { - var tokenDataMap sync.Map - // ttl > half of period - tokenDataMap.Store("testToken1", vault.TokenLookupMetadata{ - Renewable: true, - Ttl: tokenPeriod * 7 / 10, - Period: tokenPeriod, - }) - // ttl = half of period - tokenDataMap.Store("testToken2", vault.TokenLookupMetadata{ - Renewable: true, - Ttl: tokenPeriod / 2, - Period: tokenPeriod, - }) - // ttl < half of period - tokenDataMap.Store("testToken3", vault.TokenLookupMetadata{ - Renewable: true, - Ttl: tokenPeriod * 3 / 10, - Period: tokenPeriod, - }) - // to be expired token - tokenDataMap.Store("toToExpiredToken", vault.TokenLookupMetadata{ - Renewable: true, - Ttl: 1, - Period: tokenPeriod, - }) - // expired token - tokenDataMap.Store("expiredToken", vault.TokenLookupMetadata{ - Renewable: true, - Ttl: 0, - Period: tokenPeriod, - }) - // not renewable token - tokenDataMap.Store("unrenewableToken", vault.TokenLookupMetadata{ - Renewable: false, - Ttl: 0, - Period: tokenPeriod, - }) - - return &tokenDataMap -} diff --git a/bootstrap/handlers/secret/credentials.go b/bootstrap/handlers/secret/credentials.go deleted file mode 100644 index 107fb74c..00000000 --- a/bootstrap/handlers/secret/credentials.go +++ /dev/null @@ -1,40 +0,0 @@ -/******************************************************************************** - * Copyright 2019 Dell 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 "github.com/edgexfoundry/go-mod-bootstrap/config" - -const RedisDB = "redisdb" - -func (s *SecretProvider) GetDatabaseCredentials(database config.Database) (config.Credentials, error) { - // If security is disabled then we are to use the credentials supplied by the configuration. - if !s.isSecurityEnabled() { - return config.Credentials{ - Username: database.Username, - Password: database.Password, - }, nil - } - - secrets, err := s.secretClient.GetSecrets(database.Type, "username", "password") - if err != nil { - return config.Credentials{}, err - } - - return config.Credentials{ - Username: secrets["username"], - Password: secrets["password"], - }, nil - -} diff --git a/bootstrap/handlers/secret/secret.go b/bootstrap/handlers/secret/secret.go deleted file mode 100644 index 471d02f2..00000000 --- a/bootstrap/handlers/secret/secret.go +++ /dev/null @@ -1,140 +0,0 @@ -/******************************************************************************** - * Copyright 2019 Dell 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" - "fmt" - "os" - "sync" - - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/handlers/secret/client" - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" - "github.com/edgexfoundry/go-mod-bootstrap/config" - "github.com/edgexfoundry/go-mod-bootstrap/di" - - "github.com/edgexfoundry/go-mod-secrets/pkg" - "github.com/edgexfoundry/go-mod-secrets/pkg/providers/vault" - "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader" - "github.com/edgexfoundry/go-mod-secrets/pkg/token/fileioperformer" -) - -type SecretProvider struct { - secretClient pkg.SecretClient -} - -func NewSecret() *SecretProvider { - return &SecretProvider{} -} - -// BootstrapHandler creates a secretClient to be used for obtaining secrets from a SecretProvider store manager. -// NOTE: This BootstrapHandler is responsible for creating a utility that will most likely be used by other -// BootstrapHandlers to obtain sensitive data, such as database credentials. This BootstrapHandler should be processed -// before other BootstrapHandlers, possibly even first since it has not other dependencies. -func (s *SecretProvider) BootstrapHandler( - ctx context.Context, - _ *sync.WaitGroup, - startupTimer startup.Timer, - dic *di.Container) bool { - - lc := container.LoggingClientFrom(dic.Get) - - // attempt to create a new SecretProvider client only if security is enabled. - if s.isSecurityEnabled() { - var err error - - lc.Info("Creating SecretClient") - - configuration := container.ConfigurationFrom(dic.Get) - secretStoreConfig := configuration.GetBootstrap().SecretStore - - for startupTimer.HasNotElapsed() { - var secretConfig vault.SecretConfig - - lc.Info("Reading secret store configuration and authentication token") - - secretConfig, err = s.getSecretConfig(secretStoreConfig) - if err == nil { - var secretClient pkg.SecretClient - - lc.Info("Attempting to create secretclient") - secretClient, err = client.NewVault(ctx, secretConfig, lc).Get(configuration.GetBootstrap().SecretStore) - if err == nil { - s.secretClient = secretClient - lc.Info("Created SecretClient") - break - } - } - - lc.Warn(fmt.Sprintf("Retryable failure while creating SecretClient: %s", err.Error())) - startupTimer.SleepForInterval() - } - - if err != nil { - lc.Error(fmt.Sprintf("unable to create SecretClient: %s", err.Error())) - return false - } - } - - dic.Update(di.ServiceConstructorMap{ - container.CredentialsProviderName: func(get di.Get) interface{} { - return s - }, - container.CertificateProviderName: func(get di.Get) interface{} { - return s - }, - }) - - return true -} - -// getSecretConfig creates a SecretConfig based on the SecretStoreInfo configuration properties. -// If a tokenfile is present it will override the Authentication.AuthToken value. -func (s *SecretProvider) getSecretConfig(secretStoreInfo config.SecretStoreInfo) (vault.SecretConfig, error) { - secretConfig := vault.SecretConfig{ - Host: secretStoreInfo.Host, - Port: secretStoreInfo.Port, - Path: secretStoreInfo.Path, - Protocol: secretStoreInfo.Protocol, - Namespace: secretStoreInfo.Namespace, - RootCaCertPath: secretStoreInfo.RootCaCertPath, - ServerName: secretStoreInfo.ServerName, - Authentication: secretStoreInfo.Authentication, - AdditionalRetryAttempts: secretStoreInfo.AdditionalRetryAttempts, - RetryWaitPeriod: secretStoreInfo.RetryWaitPeriod, - } - - if !s.isSecurityEnabled() || secretStoreInfo.TokenFile == "" { - return secretConfig, nil - } - - // only bother getting a token if security is enabled and the configuration-provided tokenfile is not empty. - fileIoPerformer := fileioperformer.NewDefaultFileIoPerformer() - authTokenLoader := authtokenloader.NewAuthTokenLoader(fileIoPerformer) - - token, err := authTokenLoader.Load(secretStoreInfo.TokenFile) - if err != nil { - return secretConfig, err - } - secretConfig.Authentication.AuthToken = token - return secretConfig, nil -} - -// isSecurityEnabled determines if security has been enabled. -func (s *SecretProvider) isSecurityEnabled() bool { - env := os.Getenv("EDGEX_SECURITY_SECRET_STORE") - return env != "false" -} diff --git a/bootstrap/interfaces/configuration.go b/bootstrap/interfaces/configuration.go index 94a7cb78..7d940fca 100644 --- a/bootstrap/interfaces/configuration.go +++ b/bootstrap/interfaces/configuration.go @@ -39,4 +39,7 @@ type Configuration interface { // GetRegistryInfo gets the config.RegistryInfo field from the ConfigurationStruct. GetRegistryInfo() config.RegistryInfo + + // GetInsecureSecrets gets the config.InsecureSecrets field from the ConfigurationStruct. + GetInsecureSecrets() config.InsecureSecrets } diff --git a/bootstrap/interfaces/mocks/SecretProvider.go b/bootstrap/interfaces/mocks/SecretProvider.go new file mode 100644 index 00000000..e064f964 --- /dev/null +++ b/bootstrap/interfaces/mocks/SecretProvider.go @@ -0,0 +1,91 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" +) + +// SecretProvider is an autogenerated mock type for the SecretProvider type +type SecretProvider struct { + mock.Mock +} + +// GetSecrets provides a mock function with given fields: path, keys +func (_m *SecretProvider) GetSecrets(path string, keys ...string) (map[string]string, error) { + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, path) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[string]string + if rf, ok := ret.Get(0).(func(string, ...string) map[string]string); ok { + r0 = rf(path, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...string) error); ok { + r1 = rf(path, keys...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InsecureSecretsUpdated provides a mock function with given fields: +func (_m *SecretProvider) InsecureSecretsUpdated() { + _m.Called() +} + +// IsSecurityEnabled provides a mock function with given fields: +func (_m *SecretProvider) IsSecurityEnabled() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// SecretsLastUpdated provides a mock function with given fields: +func (_m *SecretProvider) SecretsLastUpdated() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +// StoreSecrets provides a mock function with given fields: path, secrets +func (_m *SecretProvider) StoreSecrets(path string, secrets map[string]string) error { + ret := _m.Called(path, secrets) + + var r0 error + if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok { + r0 = rf(path, secrets) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/bootstrap/interfaces/secret.go b/bootstrap/interfaces/secret.go new file mode 100644 index 00000000..10fae4a8 --- /dev/null +++ b/bootstrap/interfaces/secret.go @@ -0,0 +1,21 @@ +package interfaces + +import "time" + +type SecretProvider interface { + // StoreSecrets stores new secrets into the service's SecretStore at the specified path. + StoreSecrets(path string, secrets map[string]string) error + + // GetSecrets retrieves secrets from the service's SecretStore at the specified path. + GetSecrets(path string, keys ...string) (map[string]string, error) + + // InsecureSecretsUpdated sets the secrets last updated time to current time. + // ignored if in secure mode. Needed for InsecureSecrets support in non-secure mode. + InsecureSecretsUpdated() + + // 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/secret/handler.go b/bootstrap/secret/handler.go new file mode 100644 index 00000000..362e91d3 --- /dev/null +++ b/bootstrap/secret/handler.go @@ -0,0 +1,125 @@ +/******************************************************************************** + * Copyright 2019 Dell 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" + "fmt" + "sync" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/di" + "github.com/edgexfoundry/go-mod-secrets/pkg/types" + "github.com/edgexfoundry/go-mod-secrets/secrets" + + "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader" + "github.com/edgexfoundry/go-mod-secrets/pkg/token/fileioperformer" +) + +// BootstrapHandler full initializes the Provider store manager. +func (p *Provider) BootstrapHandler( + ctx context.Context, + _ *sync.WaitGroup, + startupTimer startup.Timer, + dic *di.Container) bool { + + p.lc = container.LoggingClientFrom(dic.Get) + p.configuration = container.ConfigurationFrom(dic.Get) + + // attempt to create a new SecretProvider client only if security is enabled. + if p.IsSecurityEnabled() { + var err error + + p.lc.Info("Creating SecretClient") + + secretStoreConfig := p.configuration.GetBootstrap().SecretStore + + for startupTimer.HasNotElapsed() { + var secretConfig types.SecretConfig + + p.lc.Info("Reading secret store configuration and authentication token") + + secretConfig, err = p.getSecretConfig(secretStoreConfig, dic) + if err == nil { + var secretClient secrets.SecretClient + + p.lc.Info("Attempting to create secret client") + secretClient, err = secrets.NewClient(ctx, secretConfig, p.lc, p.defaultTokenExpiredCallback) + if err == nil { + p.secretClient = secretClient + p.lc.Info("Created SecretClient") + break + } + } + + p.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())) + return false + } + } + + dic.Update(di.ServiceConstructorMap{ + container.SecretProviderName: func(get di.Get) interface{} { + return p + }, + }) + + return true +} + +// 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) { + secretConfig := types.SecretConfig{ + Host: secretStoreInfo.Host, + Port: secretStoreInfo.Port, + Path: secretStoreInfo.Path, + Protocol: secretStoreInfo.Protocol, + Namespace: secretStoreInfo.Namespace, + RootCaCertPath: secretStoreInfo.RootCaCertPath, + ServerName: secretStoreInfo.ServerName, + Authentication: secretStoreInfo.Authentication, + AdditionalRetryAttempts: secretStoreInfo.AdditionalRetryAttempts, + RetryWaitPeriod: secretStoreInfo.RetryWaitPeriod, + } + + if !p.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/secret/handler_test.go b/bootstrap/secret/handler_test.go new file mode 100644 index 00000000..56854e2f --- /dev/null +++ b/bootstrap/secret/handler_test.go @@ -0,0 +1,68 @@ +/******************************************************************************* + * 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/provider.go b/bootstrap/secret/provider.go new file mode 100644 index 00000000..22313239 --- /dev/null +++ b/bootstrap/secret/provider.go @@ -0,0 +1,258 @@ +/******************************************************************************* + * Copyright 2018 Dell Inc. + * 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" + + "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 { + secretClient secrets.SecretClient + lc logger.LoggingClient + 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(), + } +} + +// 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 +} + +// 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...) + } + + if cachedSecrets := p.getSecretsCache(path, keys...); cachedSecrets != nil { + return cachedSecrets, nil + } + + if p.secretClient == nil { + return nil, errors.New("can't get secret(p), secret client is not properly initialized") + } + + secrets, 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 +} + +func (p *Provider) getSecretsCache(path string, keys ...string) map[string]string { + secrets := make(map[string]string) + + // Synchronize cache access + p.cacheMutex.Lock() + defer p.cacheMutex.Unlock() + + // check cache for keys + allKeysExistInCache := false + cachedSecrets, cacheExists := p.secretsCache[path] + value := "" + + if cacheExists { + for _, key := range keys { + value, allKeysExistInCache = cachedSecrets[key] + if !allKeysExistInCache { + return nil + } + secrets[key] = value + } + + // return secrets if the requested keys exist in cache + if allKeysExistInCache { + return secrets + } + } + + return nil +} + +func (p *Provider) updateSecretsCache(path string, secrets map[string]string) { + // Synchronize cache access + p.cacheMutex.Lock() + defer p.cacheMutex.Unlock() + + if _, cacheExists := p.secretsCache[path]; !cacheExists { + p.secretsCache[path] = secrets + } + + for key, value := range secrets { + p.secretsCache[path][key] = value + } +} + +// StoreSecrets stores the secrets to a secret store. +// 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") + } + + if p.secretClient == nil { + return errors.New("can't store secret(p) 'SecretProvider' is not properly initialized") + } + + err := p.secretClient.StoreSecrets(path, secrets) + if err != nil { + return err + } + + // Synchronize cache access before clearing + p.cacheMutex.Lock() + // Clearing cache because adding a new secret(p) possibly invalidates the previous cache + p.secretsCache = make(map[string]map[string]string) + p.cacheMutex.Unlock() + //indicate to the SDK that the cache has been invalidated + p.lastUpdated = time.Now() + 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() + } +} + +func (p *Provider) 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) { + 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) + if err != nil { + p.lc.Error(fmt.Sprintf("fail to load auth token from tokenFile %s: %v", tokenFile, err)) + return "", false + } + + if reReadToken == expiredToken { + p.lc.Error("No new replacement token found for the expired token") + return reReadToken, false + } + + return reReadToken, true +} diff --git a/bootstrap/secret/provider_test.go b/bootstrap/secret/provider_test.go new file mode 100644 index 00000000..9caa3461 --- /dev/null +++ b/bootstrap/secret/provider_test.go @@ -0,0 +1,252 @@ +/******************************************************************************* + * 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" + "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" + "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) { + expected := map[string]string{"username": "admin", "password": "sam123!"} + + mock := &mocks.SecretClient{} + mock.On("GetSecrets", "redis", "username", "password").Return(expected, nil) + mock.On("GetSecrets", "redis").Return(expected, nil) + 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}, + } + + 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{}) + 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 TestProvider_GetSecrets_SecureCached(t *testing.T) { + os.Setenv(EnvSecretStore, "true") + 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{}) + actual, err := target.GetSecrets("redis", "username", "password") + require.NoError(t, err) + assert.Equal(t, expected, actual) + + // Now have mock return error if it is called which should not happen of secrets are cached + mock.On("GetSecrets", "redis", "username", "password").Return(nil, errors.New("No Cached")) + actual, err = target.GetSecrets("redis", "username", "password") + require.NoError(t, err) + assert.Equal(t, expected, actual) + + // Now check for error when not all requested keys not in cache. + mock.On("GetSecrets", "redis", "username", "password2").Return(nil, errors.New("No Cached")) + actual, err = target.GetSecrets("redis", "username", "password2") + require.Error(t, err) +} + +func TestProvider_GetSecrets_SecureCached_Invalidated(t *testing.T) { + os.Setenv(EnvSecretStore, "true") + 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() + mock.On("StoreSecrets", "redis", expected).Return(nil) + + target := NewProviderWithDependents(mock, nil, logger.MockLogger{}) + actual, err := target.GetSecrets("redis", "username", "password") + require.NoError(t, err) + assert.Equal(t, expected, actual) + + // Invalidate the secrets cache by storing new secrets + err = target.StoreSecrets("redis", expected) + require.NoError(t, err) + + // Now have mock return error is it is called which should now happen if the cache was properly invalidated by the above call to StoreSecrets + mock.On("GetSecrets", "redis", "username", "password").Return(nil, errors.New("No Cached")) + actual, err = target.GetSecrets("redis", "username", "password") + require.Error(t, err) +} + +func TestProvider_StoreSecrets_Secure(t *testing.T) { + input := map[string]string{"username": "admin", "password": "sam123!"} + mock := &mocks.SecretClient{} + mock.On("StoreSecrets", "redis", input).Return(nil) + mock.On("StoreSecrets", "error", input).Return(errors.New("Some error happened")) + + tests := []struct { + Name string + Secure string + Path string + Client secrets.SecretClient + ExpectError bool + }{ + {"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{}) + err := target.StoreSecrets(tc.Path, input) + if tc.ExpectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +func TestProvider_SecretsLastUpdated(t *testing.T) { + os.Setenv(EnvSecretStore, "true") + + input := map[string]string{"username": "admin", "password": "sam123!"} + mock := &mocks.SecretClient{} + mock.On("StoreSecrets", "redis", input).Return(nil) + + target := NewProviderWithDependents(mock, nil, logger.MockLogger{}) + + previous := target.SecretsLastUpdated() + time.Sleep(1 * time.Second) + err := target.StoreSecrets("redis", input) + require.NoError(t, err) + current := target.SecretsLastUpdated() + assert.True(t, current.After(previous)) +} + +func TestProvider_InsecureSecretsUpdated(t *testing.T) { + os.Setenv(EnvSecretStore, "false") + target := NewProviderWithDependents(nil, nil, logger.MockLogger{}) + previous := target.SecretsLastUpdated() + time.Sleep(1 * time.Second) + target.InsecureSecretsUpdated() + current := target.SecretsLastUpdated() + assert.True(t, current.After(previous)) +} + +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(rawConfig interface{}) bool { + panic("implement me") +} + +func (t TestConfig) EmptyWritablePtr() interface{} { + panic("implement me") +} + +func (t TestConfig) UpdateWritableFromRaw(rawWritable 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 t.InsecureSecrets +} diff --git a/config/types.go b/config/types.go index a6d455dd..8ff6ad1d 100644 --- a/config/types.go +++ b/config/types.go @@ -19,8 +19,7 @@ import ( "time" "github.com/edgexfoundry/go-mod-core-contracts/clients" - - "github.com/edgexfoundry/go-mod-secrets/pkg/providers/vault" + "github.com/edgexfoundry/go-mod-secrets/pkg/types" ) // ServiceInfo contains configuration settings necessary for the basic operation of any EdgeX service. @@ -108,7 +107,7 @@ type SecretStoreInfo struct { Namespace string RootCaCertPath string ServerName string - Authentication vault.AuthenticationInfo + Authentication types.AuthenticationInfo AdditionalRetryAttempts int RetryWaitPeriod string retryWaitPeriodTime time.Duration @@ -117,13 +116,11 @@ type SecretStoreInfo struct { } type Database struct { - Username string - Password string - Type string - Timeout int - Host string - Port int - Name string + Type string + Timeout int + Host string + Port int + Name string } // Credentials encapsulates username-password attributes. @@ -138,6 +135,15 @@ type CertKeyPair struct { Key string } +// InsecureSecrets is used to hold the secrets stored in the configuration +type InsecureSecrets map[string]InsecureSecretsInfo + +// InsecureSecretsInfo encapsulates info used to retrieve insecure secrets +type InsecureSecretsInfo struct { + Path string + Secrets map[string]string +} + // BootstrapConfiguration defines the configuration elements required by the bootstrap. type BootstrapConfiguration struct { Clients map[string]ClientInfo diff --git a/go.mod b/go.mod index 08b933c5..0d656e84 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,9 @@ require ( github.com/edgexfoundry/go-mod-secrets v0.0.27 github.com/gorilla/mux v1.7.1 github.com/pelletier/go-toml v1.2.0 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.6.1 ) +replace github.com/edgexfoundry/go-mod-secrets => ../go-mod-secrets + go 1.15 From e69887126c6d64e5dba78b71e0367ef7815b3eab Mon Sep 17 00:00:00 2001 From: lenny Date: Mon, 14 Dec 2020 10:47:05 -0700 Subject: [PATCH 2/5] refactor: Fixup from PR review comments Signed-off-by: lenny --- bootstrap/config/config.go | 10 ++++++---- bootstrap/container/secret.go | 2 +- bootstrap/container/token.go | 4 ++-- bootstrap/interfaces/secret.go | 7 ++++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/bootstrap/config/config.go b/bootstrap/config/config.go index a5c10573..1cf223a0 100644 --- a/bootstrap/config/config.go +++ b/bootstrap/config/config.go @@ -314,12 +314,14 @@ func (cp *Processor) listenForChanges(serviceConfig interfaces.Configuration, co lc.Info("Insecure Secrets have been updated") secretProvider := container.SecretProviderFrom(cp.dic.Get) if secretProvider != nil { - secretProvider.InsecureSecretsUpdated() + secretProvider.SecretsUpdated() } - } - if cp.configUpdated != nil { - cp.configUpdated <- struct{}{} + default: + // Signal that configuration updates exists that have not already been processed. + if cp.configUpdated != nil { + cp.configUpdated <- struct{}{} + } } } } diff --git a/bootstrap/container/secret.go b/bootstrap/container/secret.go index ab155d69..7e89f5b7 100644 --- a/bootstrap/container/secret.go +++ b/bootstrap/container/secret.go @@ -32,7 +32,7 @@ func SecretClientFrom(get di.Get) secrets.SecretClient { // SecretProviderName contains the name of the interfaces.SecretProvider implementation in the DIC. var SecretProviderName = di.TypeInstanceToName((*interfaces.SecretProvider)(nil)) -// CredentialsProviderFrom helper function queries the DIC and returns the interfaces.CredentialsProviderName +// SecretProviderFrom helper function queries the DIC and returns the interfaces.SecretProvider // implementation. func SecretProviderFrom(get di.Get) interfaces.SecretProvider { client := get(SecretProviderName).(interfaces.SecretProvider) diff --git a/bootstrap/container/token.go b/bootstrap/container/token.go index 16ab0682..fa518832 100644 --- a/bootstrap/container/token.go +++ b/bootstrap/container/token.go @@ -32,10 +32,10 @@ func FileIoPerformerFrom(get di.Get) fileioperformer.FileIoPerformer { return (fileioperformer.FileIoPerformer)(nil) } -// FileIoPerformerInterfaceName contains the name of the fileioperformer.FileIoPerformer implementation in the DIC. +// AuthTokenLoaderInterfaceName contains the name of the authtokenloader.AuthTokenLoader implementation in the DIC. var AuthTokenLoaderInterfaceName = di.TypeInstanceToName((*authtokenloader.AuthTokenLoader)(nil)) -// FileIoPerformerFrom helper function queries the DIC and returns the fileioperformer.FileIoPerformer implementation. +// AuthTokenLoaderFrom helper function queries the DIC and returns the authtokenloader.AuthTokenLoader implementation. func AuthTokenLoaderFrom(get di.Get) authtokenloader.AuthTokenLoader { loader := get(AuthTokenLoaderInterfaceName) if loader != nil { diff --git a/bootstrap/interfaces/secret.go b/bootstrap/interfaces/secret.go index 10fae4a8..0d3f2a4a 100644 --- a/bootstrap/interfaces/secret.go +++ b/bootstrap/interfaces/secret.go @@ -2,6 +2,8 @@ package interfaces import "time" +// SecretProvider defines the contract for secret provider implementations that +// allow secrets to be retrieved/stored from/to a services Secret Store. type SecretProvider interface { // StoreSecrets stores new secrets into the service's SecretStore at the specified path. StoreSecrets(path string, secrets map[string]string) error @@ -9,9 +11,8 @@ type SecretProvider interface { // GetSecrets retrieves secrets from the service's SecretStore at the specified path. GetSecrets(path string, keys ...string) (map[string]string, error) - // InsecureSecretsUpdated sets the secrets last updated time to current time. - // ignored if in secure mode. Needed for InsecureSecrets support in non-secure mode. - InsecureSecretsUpdated() + // SecretsUpdated sets the secrets last updated time to current time. + SecretsUpdated() // SecretsLastUpdated returns the last time secrets were updated SecretsLastUpdated() time.Time From a8a073e08b700e62b80be4e67731c2c1fff868a7 Mon Sep 17 00:00:00 2001 From: lenny Date: Mon, 14 Dec 2020 17:12:27 -0700 Subject: [PATCH 3/5] refactor: Refactored SecretProvider into seperate Secure & Insecure implementations Signed-off-by: lenny --- bootstrap/container/configuration.go | 7 +- bootstrap/container/secret.go | 15 +- bootstrap/environment/variables_test.go | 16 +- .../handlers/{httpserver => }/httpserver.go | 6 +- bootstrap/handlers/{message => }/message.go | 6 +- bootstrap/handlers/{testing => }/ready.go | 6 +- .../{secret/handler.go => handlers/secret.go} | 66 +++---- bootstrap/handlers/secret_test.go | 173 ++++++++++++++++++ bootstrap/interfaces/mocks/SecretProvider.go | 24 +-- bootstrap/interfaces/secret.go | 3 - bootstrap/registration/registry_test.go | 4 + bootstrap/secret/handler_test.go | 68 ------- bootstrap/secret/helper.go | 29 +++ bootstrap/secret/insecure.go | 108 +++++++++++ bootstrap/secret/insecure_test.go | 88 +++++++++ bootstrap/secret/{provider.go => secure.go} | 144 ++++----------- .../{provider_test.go => secure_test.go} | 136 +++++++------- go.mod | 6 +- 18 files changed, 571 insertions(+), 334 deletions(-) rename bootstrap/handlers/{httpserver => }/httpserver.go (94%) rename bootstrap/handlers/{message => }/message.go (91%) rename bootstrap/handlers/{testing => }/ready.go (92%) rename bootstrap/{secret/handler.go => handlers/secret.go} (61%) create mode 100644 bootstrap/handlers/secret_test.go delete mode 100644 bootstrap/secret/handler_test.go create mode 100644 bootstrap/secret/helper.go create mode 100644 bootstrap/secret/insecure.go create mode 100644 bootstrap/secret/insecure_test.go rename bootstrap/secret/{provider.go => secure.go} (50%) rename bootstrap/secret/{provider_test.go => secure_test.go} (67%) diff --git a/bootstrap/container/configuration.go b/bootstrap/container/configuration.go index 3dbd661d..66ba3bfd 100644 --- a/bootstrap/container/configuration.go +++ b/bootstrap/container/configuration.go @@ -24,5 +24,10 @@ var ConfigurationInterfaceName = di.TypeInstanceToName((*interfaces.Configuratio // ConfigurationFrom helper function queries the DIC and returns the interfaces.Configuration implementation. func ConfigurationFrom(get di.Get) interfaces.Configuration { - return get(ConfigurationInterfaceName).(interfaces.Configuration) + configuration := get(ConfigurationInterfaceName) + if configuration != nil { + return configuration.(interfaces.Configuration) + } + + return (interfaces.Configuration)(nil) } diff --git a/bootstrap/container/secret.go b/bootstrap/container/secret.go index 7e89f5b7..3821a7f9 100644 --- a/bootstrap/container/secret.go +++ b/bootstrap/container/secret.go @@ -18,26 +18,17 @@ package container import ( "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" "github.com/edgexfoundry/go-mod-bootstrap/di" - "github.com/edgexfoundry/go-mod-secrets/secrets" ) -// SecretClientName contains the name of the registry.Client implementation in the DIC. -var SecretClientName = di.TypeInstanceToName((*secrets.SecretClient)(nil)) - -// SecretClientFrom helper function queries the DIC and returns the pkg.SecretClient implementation. -func SecretClientFrom(get di.Get) secrets.SecretClient { - return get(SecretClientName).(secrets.SecretClient) -} - // SecretProviderName contains the name of the interfaces.SecretProvider implementation in the DIC. var SecretProviderName = di.TypeInstanceToName((*interfaces.SecretProvider)(nil)) // SecretProviderFrom helper function queries the DIC and returns the interfaces.SecretProvider // implementation. func SecretProviderFrom(get di.Get) interfaces.SecretProvider { - client := get(SecretProviderName).(interfaces.SecretProvider) - if client != nil { - return client.(interfaces.SecretProvider) + provider := get(SecretProviderName).(interfaces.SecretProvider) + if provider != nil { + return provider.(interfaces.SecretProvider) } return (interfaces.SecretProvider)(nil) } 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/mocks/SecretProvider.go b/bootstrap/interfaces/mocks/SecretProvider.go index e064f964..455e33bc 100644 --- a/bootstrap/interfaces/mocks/SecretProvider.go +++ b/bootstrap/interfaces/mocks/SecretProvider.go @@ -43,25 +43,6 @@ func (_m *SecretProvider) GetSecrets(path string, keys ...string) (map[string]st return r0, r1 } -// InsecureSecretsUpdated provides a mock function with given fields: -func (_m *SecretProvider) InsecureSecretsUpdated() { - _m.Called() -} - -// IsSecurityEnabled provides a mock function with given fields: -func (_m *SecretProvider) IsSecurityEnabled() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - // SecretsLastUpdated provides a mock function with given fields: func (_m *SecretProvider) SecretsLastUpdated() time.Time { ret := _m.Called() @@ -76,6 +57,11 @@ func (_m *SecretProvider) SecretsLastUpdated() time.Time { return r0 } +// SecretsUpdated provides a mock function with given fields: +func (_m *SecretProvider) SecretsUpdated() { + _m.Called() +} + // StoreSecrets provides a mock function with given fields: path, secrets func (_m *SecretProvider) StoreSecrets(path string, secrets map[string]string) error { ret := _m.Called(path, secrets) 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") } diff --git a/go.mod b/go.mod index 0d656e84..5f5e5d77 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,12 @@ module github.com/edgexfoundry/go-mod-bootstrap require ( github.com/BurntSushi/toml v0.3.1 github.com/edgexfoundry/go-mod-configuration v0.0.8 - github.com/edgexfoundry/go-mod-core-contracts v0.1.115 + github.com/edgexfoundry/go-mod-core-contracts v0.1.129 github.com/edgexfoundry/go-mod-registry v0.1.26 - github.com/edgexfoundry/go-mod-secrets v0.0.27 + github.com/edgexfoundry/go-mod-secrets v0.0.29 github.com/gorilla/mux v1.7.1 github.com/pelletier/go-toml v1.2.0 github.com/stretchr/testify v1.6.1 ) -replace github.com/edgexfoundry/go-mod-secrets => ../go-mod-secrets - go 1.15 From 3af2dc0d084eac7b818430bf639e19e1df790f24 Mon Sep 17 00:00:00 2001 From: lenny Date: Tue, 15 Dec 2020 16:32:04 -0700 Subject: [PATCH 4/5] refactor: Fixup for PR comments Signed-off-by: lenny --- bootstrap/secret/secure.go | 6 +++--- bootstrap/secret/secure_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bootstrap/secret/secure.go b/bootstrap/secret/secure.go index 5899f5fb..fe90ba8b 100644 --- a/bootstrap/secret/secure.go +++ b/bootstrap/secret/secure.go @@ -28,7 +28,7 @@ import ( "time" ) -// Provider implements the SecretProvider interface +// SecureProvider implements the SecretProvider interface type SecureProvider struct { secretClient secrets.SecretClient lc logger.LoggingClient @@ -67,7 +67,7 @@ func (p *SecureProvider) GetSecrets(path string, keys ...string) (map[string]str } if p.secretClient == nil { - return nil, errors.New("can't get secret(p), secret client is not properly initialized") + return nil, errors.New("can't get secrets. Secure secret provider is not properly initialized") } secureSecrets, err := p.secretClient.GetSecrets(path, keys...) @@ -129,7 +129,7 @@ func (p *SecureProvider) updateSecretsCache(path string, secrets map[string]stri // secrets map specifies the "key": "value" pairs of secrets to store 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") + return errors.New("can't store secrets. Secure secret provider is not properly initialized") } err := p.secretClient.StoreSecrets(path, secrets) diff --git a/bootstrap/secret/secure_test.go b/bootstrap/secret/secure_test.go index 47952063..0267dcde 100644 --- a/bootstrap/secret/secure_test.go +++ b/bootstrap/secret/secure_test.go @@ -89,7 +89,7 @@ func TestSecureProvider_GetSecrets_Cached(t *testing.T) { // Now check for error when not all requested keys not in cache. mock.On("GetSecrets", "redis", "username", "password2").Return(nil, errors.New("No Cached")) - actual, err = target.GetSecrets("redis", "username", "password2") + _, err = target.GetSecrets("redis", "username", "password2") require.Error(t, err) } @@ -114,7 +114,7 @@ func TestSecureProvider_GetSecrets_Cached_Invalidated(t *testing.T) { // Now have mock return error is it is called which should now happen if the cache was properly invalidated by the above call to StoreSecrets mock.On("GetSecrets", "redis", "username", "password").Return(nil, errors.New("No Cached")) - actual, err = target.GetSecrets("redis", "username", "password") + _, err = target.GetSecrets("redis", "username", "password") require.Error(t, err) } From 0169b9f66363a8e0fc5880883a0cf3f38bc0e9ba Mon Sep 17 00:00:00 2001 From: lenny Date: Sat, 19 Dec 2020 09:47:11 -0700 Subject: [PATCH 5/5] refactor: Fixup form PR review for Cloud Reworked container DIC get functions Swith to use RWMutex for secrest cache. Signed-off-by: lenny --- bootstrap/container/configuration.go | 8 +++---- bootstrap/container/logging.go | 11 +++++---- bootstrap/container/registry.go | 9 +++---- bootstrap/container/secret.go | 9 +++---- bootstrap/container/token.go | 35 ++++++++++++++-------------- bootstrap/secret/secure.go | 8 +++---- 6 files changed, 42 insertions(+), 38 deletions(-) diff --git a/bootstrap/container/configuration.go b/bootstrap/container/configuration.go index 66ba3bfd..2deb0ffb 100644 --- a/bootstrap/container/configuration.go +++ b/bootstrap/container/configuration.go @@ -24,10 +24,10 @@ var ConfigurationInterfaceName = di.TypeInstanceToName((*interfaces.Configuratio // ConfigurationFrom helper function queries the DIC and returns the interfaces.Configuration implementation. func ConfigurationFrom(get di.Get) interfaces.Configuration { - configuration := get(ConfigurationInterfaceName) - if configuration != nil { - return configuration.(interfaces.Configuration) + configuration, ok := get(ConfigurationInterfaceName).(interfaces.Configuration) + if !ok { + return nil } - return (interfaces.Configuration)(nil) + return configuration } diff --git a/bootstrap/container/logging.go b/bootstrap/container/logging.go index 4641c880..b37f3081 100644 --- a/bootstrap/container/logging.go +++ b/bootstrap/container/logging.go @@ -15,8 +15,9 @@ package container import ( - "github.com/edgexfoundry/go-mod-bootstrap/di" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + + "github.com/edgexfoundry/go-mod-bootstrap/di" ) // LoggingClientInterfaceName contains the name of the logger.LoggingClient implementation in the DIC. @@ -24,10 +25,10 @@ var LoggingClientInterfaceName = di.TypeInstanceToName((*logger.LoggingClient)(n // LoggingClientFrom helper function queries the DIC and returns the logger.loggingClient implementation. func LoggingClientFrom(get di.Get) logger.LoggingClient { - loggingClient := get(LoggingClientInterfaceName) - if loggingClient != nil { - return loggingClient.(logger.LoggingClient) + loggingClient, ok := get(LoggingClientInterfaceName).(logger.LoggingClient) + if !ok { + return nil } - return (logger.LoggingClient)(nil) + return loggingClient } diff --git a/bootstrap/container/registry.go b/bootstrap/container/registry.go index 4dc4f787..ba683124 100644 --- a/bootstrap/container/registry.go +++ b/bootstrap/container/registry.go @@ -25,9 +25,10 @@ var RegistryClientInterfaceName = di.TypeInstanceToName((*registry.Client)(nil)) // RegistryFrom helper function queries the DIC and returns the registry.Client implementation. func RegistryFrom(get di.Get) registry.Client { - registryClient := get(RegistryClientInterfaceName) - if registryClient != nil { - return registryClient.(registry.Client) + registryClient, ok := get(RegistryClientInterfaceName).(registry.Client) + if !ok { + return nil } - return (registry.Client)(nil) + + return registryClient } diff --git a/bootstrap/container/secret.go b/bootstrap/container/secret.go index 3821a7f9..d9fd96df 100644 --- a/bootstrap/container/secret.go +++ b/bootstrap/container/secret.go @@ -26,9 +26,10 @@ var SecretProviderName = di.TypeInstanceToName((*interfaces.SecretProvider)(nil) // SecretProviderFrom helper function queries the DIC and returns the interfaces.SecretProvider // implementation. func SecretProviderFrom(get di.Get) interfaces.SecretProvider { - provider := get(SecretProviderName).(interfaces.SecretProvider) - if provider != nil { - return provider.(interfaces.SecretProvider) + provider, ok := get(SecretProviderName).(interfaces.SecretProvider) + if !ok { + return nil } - return (interfaces.SecretProvider)(nil) + + return provider } diff --git a/bootstrap/container/token.go b/bootstrap/container/token.go index fa518832..7ca83b9c 100644 --- a/bootstrap/container/token.go +++ b/bootstrap/container/token.go @@ -15,31 +15,32 @@ package container import ( - "github.com/edgexfoundry/go-mod-bootstrap/di" "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader" - "github.com/edgexfoundry/go-mod-secrets/pkg/token/fileioperformer" + + "github.com/edgexfoundry/go-mod-bootstrap/di" ) -// FileIoPerformerInterfaceName contains the name of the fileioperformer.FileIoPerformer implementation in the DIC. -var FileIoPerformerInterfaceName = di.TypeInstanceToName((*fileioperformer.FileIoPerformer)(nil)) - -// FileIoPerformerFrom helper function queries the DIC and returns the fileioperformer.FileIoPerformer implementation. -func FileIoPerformerFrom(get di.Get) fileioperformer.FileIoPerformer { - fileIo := get(FileIoPerformerInterfaceName) - if fileIo != nil { - return fileIo.(fileioperformer.FileIoPerformer) - } - return (fileioperformer.FileIoPerformer)(nil) -} +//// FileIoPerformerInterfaceName contains the name of the fileioperformer.FileIoPerformer implementation in the DIC. +//var FileIoPerformerInterfaceName = di.TypeInstanceToName((*fileioperformer.FileIoPerformer)(nil)) +// +//// FileIoPerformerFrom helper function queries the DIC and returns the fileioperformer.FileIoPerformer implementation. +//func FileIoPerformerFrom(get di.Get) fileioperformer.FileIoPerformer { +// fileIo := get(FileIoPerformerInterfaceName) +// if fileIo != nil { +// return fileIo.(fileioperformer.FileIoPerformer) +// } +// return (fileioperformer.FileIoPerformer)(nil) +//} // AuthTokenLoaderInterfaceName contains the name of the authtokenloader.AuthTokenLoader implementation in the DIC. var AuthTokenLoaderInterfaceName = di.TypeInstanceToName((*authtokenloader.AuthTokenLoader)(nil)) // AuthTokenLoaderFrom helper function queries the DIC and returns the authtokenloader.AuthTokenLoader implementation. func AuthTokenLoaderFrom(get di.Get) authtokenloader.AuthTokenLoader { - loader := get(AuthTokenLoaderInterfaceName) - if loader != nil { - return loader.(authtokenloader.AuthTokenLoader) + loader, ok := get(AuthTokenLoaderInterfaceName).(authtokenloader.AuthTokenLoader) + if !ok { + return nil } - return (authtokenloader.AuthTokenLoader)(nil) + + return loader } diff --git a/bootstrap/secret/secure.go b/bootstrap/secret/secure.go index fe90ba8b..8b1cb52d 100644 --- a/bootstrap/secret/secure.go +++ b/bootstrap/secret/secure.go @@ -35,7 +35,7 @@ type SecureProvider struct { loader authtokenloader.AuthTokenLoader configuration interfaces.Configuration secretsCache map[string]map[string]string // secret's path, key, value - cacheMutex *sync.Mutex + cacheMutex *sync.RWMutex lastUpdated time.Time } @@ -46,7 +46,7 @@ func NewSecureProvider(config interfaces.Configuration, lc logger.LoggingClient, lc: lc, loader: loader, secretsCache: make(map[string]map[string]string), - cacheMutex: &sync.Mutex{}, + cacheMutex: &sync.RWMutex{}, lastUpdated: time.Now(), } return provider @@ -83,8 +83,8 @@ func (p *SecureProvider) getSecretsCache(path string, keys ...string) map[string secureSecrets := make(map[string]string) // Synchronize cache access - p.cacheMutex.Lock() - defer p.cacheMutex.Unlock() + p.cacheMutex.RLock() + defer p.cacheMutex.RUnlock() // check cache for keys allKeysExistInCache := false