From 61fea9ce090b2b0bd3e6911d460f0f41984bf0dc Mon Sep 17 00:00:00 2001 From: lenny Date: Tue, 4 May 2021 17:27:03 -0700 Subject: [PATCH] feat: Add bootstrap handler to create Messaging Client with secure options Close #224 Signed-off-by: lenny --- Makefile | 2 +- bootstrap/container/messaging.go | 33 ++ bootstrap/interfaces/configuration.go | 3 + bootstrap/interfaces/mocks/SecretProvider.go | 8 +- bootstrap/interfaces/secret.go | 8 +- bootstrap/messaging/messaging.go | 229 ++++++++++++ bootstrap/messaging/messaging_test.go | 354 +++++++++++++++++++ bootstrap/messaging/testcerts_test.go | 89 +++++ bootstrap/registration/registry_test.go | 4 + bootstrap/secret/insecure.go | 8 +- bootstrap/secret/insecure_test.go | 4 +- bootstrap/secret/secret_test.go | 6 +- bootstrap/secret/secure.go | 8 +- bootstrap/secret/secure_test.go | 18 +- config/types.go | 35 ++ go.mod | 3 +- 16 files changed, 782 insertions(+), 30 deletions(-) create mode 100644 bootstrap/container/messaging.go create mode 100644 bootstrap/messaging/messaging.go create mode 100644 bootstrap/messaging/messaging_test.go create mode 100644 bootstrap/messaging/testcerts_test.go diff --git a/Makefile b/Makefile index b69a09ba..fcad201d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: test -GO=CGO_ENABLED=0 GO111MODULE=on go +GO=CGO_ENABLED=1 GO111MODULE=on go test: $(GO) test ./... -coverprofile=coverage.out ./... diff --git a/bootstrap/container/messaging.go b/bootstrap/container/messaging.go new file mode 100644 index 00000000..83d2d11e --- /dev/null +++ b/bootstrap/container/messaging.go @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright 2021 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/v2/di" + "github.com/edgexfoundry/go-mod-messaging/v2/messaging" +) + +// MessagingClientName contains the name of the messaging.MessageClient implementation in the DIC. +var MessagingClientName = di.TypeInstanceToName((*messaging.MessageClient)(nil)) + +// MessagingClientFrom helper function queries the DIC and returns the messaging.MessageClient implementation. +func MessagingClientFrom(get di.Get) messaging.MessageClient { + client, ok := get(MessagingClientName).(messaging.MessageClient) + if !ok { + return nil + } + + return client +} diff --git a/bootstrap/interfaces/configuration.go b/bootstrap/interfaces/configuration.go index 123fe072..dc4b9f92 100644 --- a/bootstrap/interfaces/configuration.go +++ b/bootstrap/interfaces/configuration.go @@ -58,4 +58,7 @@ type Configuration interface { // GetInsecureSecrets gets the config.InsecureSecrets field from the ConfigurationStruct. GetInsecureSecrets() config.InsecureSecrets + + // GetMessageBusInfo gets the config.MessageBusInfo field from the ConfigurationStruct. + GetMessageBusInfo() config.MessageBusInfo } diff --git a/bootstrap/interfaces/mocks/SecretProvider.go b/bootstrap/interfaces/mocks/SecretProvider.go index d55e81c0..3232253f 100644 --- a/bootstrap/interfaces/mocks/SecretProvider.go +++ b/bootstrap/interfaces/mocks/SecretProvider.go @@ -34,8 +34,8 @@ func (_m *SecretProvider) GetAccessToken(tokenType string, serviceKey string) (s return r0, r1 } -// GetSecrets provides a mock function with given fields: path, keys -func (_m *SecretProvider) GetSecrets(path string, keys ...string) (map[string]string, error) { +// GetSecret provides a mock function with given fields: path, keys +func (_m *SecretProvider) GetSecret(path string, keys ...string) (map[string]string, error) { _va := make([]interface{}, len(keys)) for _i := range keys { _va[_i] = keys[_i] @@ -83,8 +83,8 @@ 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 { +// StoreSecret provides a mock function with given fields: path, secrets +func (_m *SecretProvider) StoreSecret(path string, secrets map[string]string) error { ret := _m.Called(path, secrets) var r0 error diff --git a/bootstrap/interfaces/secret.go b/bootstrap/interfaces/secret.go index 5e7e583a..ffdb9456 100644 --- a/bootstrap/interfaces/secret.go +++ b/bootstrap/interfaces/secret.go @@ -5,11 +5,11 @@ 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 + // StoreSecret stores new secrets into the service's SecretStore at the specified path. + StoreSecret(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) + // GetSecret retrieves secrets from the service's SecretStore at the specified path. + GetSecret(path string, keys ...string) (map[string]string, error) // SecretsUpdated sets the secrets last updated time to current time. SecretsUpdated() diff --git a/bootstrap/messaging/messaging.go b/bootstrap/messaging/messaging.go new file mode 100644 index 00000000..1adfc3b7 --- /dev/null +++ b/bootstrap/messaging/messaging.go @@ -0,0 +1,229 @@ +/******************************************************************************* + * Copyright 2021 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 messaging + +import ( + "context" + "crypto/x509" + "errors" + "fmt" + "strings" + "sync" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/config" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-messaging/v2/messaging" + "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +) + +const ( + AuthModeKey = "authmode" + SecretNameKey = "secretname" + + AuthModeNone = "none" + AuthModeUsernamePassword = "usernamepassword" + AuthModeCert = "clientcert" + AuthModeCA = "cacert" + + SecretUsernameKey = "username" + SecretPasswordKey = "password" + SecretClientKey = "clientkey" + SecretClientCert = AuthModeCert + SecretCACert = AuthModeCA + + OptionsUsernameKey = "Username" + OptionsPasswordKey = "Password" + OptionsCertPEMBlockKey = "CertPEMBlock" + OptionsKeyPEMBlockKey = "KeyPEMBlock" + OptionsCaPEMBlockKey = "CaPEMBlock" +) + +type SecretDataProvider interface { + // GetSecret retrieves secrets from the service's SecretStore at the specified path. + GetSecret(path string, keys ...string) (map[string]string, error) +} + +type SecretData struct { + Username string + Password string + KeyPemBlock []byte + CertPemBlock []byte + CaPemBlock []byte +} + +// BootstrapHandler fulfills the BootstrapHandler contract. if enabled, tt creates and initializes the Messaging client +// and adds it to the DIC +func BootstrapHandler(ctx context.Context, wg *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { + lc := container.LoggingClientFrom(dic.Get) + messageBusInfo := container.ConfigurationFrom(dic.Get).GetMessageBusInfo() + + messageBusInfo.AuthMode = strings.ToLower(strings.TrimSpace(messageBusInfo.AuthMode)) + if len(messageBusInfo.AuthMode) > 0 && messageBusInfo.AuthMode != AuthModeNone { + if err := setOptionsAuthData(&messageBusInfo, lc, dic); err != nil { + lc.Error(err.Error()) + return false + } + } + + msgClient, err := messaging.NewMessageClient( + types.MessageBusConfig{ + PublishHost: types.HostInfo{ + Host: messageBusInfo.Host, + Port: messageBusInfo.Port, + Protocol: messageBusInfo.Protocol, + }, + Type: messageBusInfo.Type, + Optional: messageBusInfo.Optional, + }) + + if err != nil { + lc.Errorf("Failed to create MessageClient: %v", err) + return false + } + + for startupTimer.HasNotElapsed() { + select { + case <-ctx.Done(): + return false + default: + err = msgClient.Connect() + if err != nil { + lc.Warnf("Unable to connect MessageBus: %v", err) + } else { + wg.Add(1) + go func() { + defer wg.Done() + select { + case <-ctx.Done(): + _ = msgClient.Disconnect() + lc.Infof("Disconnecting from MessageBus") + } + }() + dic.Update(di.ServiceConstructorMap{ + container.MessagingClientName: func(get di.Get) interface{} { + return msgClient + }, + }) + + lc.Info(fmt.Sprintf( + "Connected to %s Message Bus @ %s://%s:%d publishing on '%s' prefix topic with AuthMode='%s'", + messageBusInfo.Type, + messageBusInfo.Protocol, + messageBusInfo.Host, + messageBusInfo.Port, + messageBusInfo.PublishTopicPrefix, + messageBusInfo.AuthMode)) + + return true + } + startupTimer.SleepForInterval() + } + } + + lc.Error("Connecting to MessageBus time out") + return false +} + +func setOptionsAuthData(messageBusInfo *config.MessageBusInfo, lc logger.LoggingClient, dic *di.Container) error { + lc.Infof("Setting options for secure MessageBus with AuthMode='%s' and SecretName='%s", + messageBusInfo.AuthMode, + messageBusInfo.SecretName) + + secretProvider := container.SecretProviderFrom(dic.Get) + if secretProvider == nil { + return errors.New("secret provider is missing. Make sure it is specified to be used in bootstrap.Run()") + } + + secretData, err := GetSecretData(messageBusInfo.AuthMode, messageBusInfo.SecretName, secretProvider) + if err != nil { + return fmt.Errorf("Unable to get Secret Data for secure message bus: %w", err) + } + + if err := ValidateSecretData(messageBusInfo.AuthMode, messageBusInfo.SecretName, secretData); err != nil { + return fmt.Errorf("Secret Data for secure message bus invalid: %w", err) + } + + if messageBusInfo.Optional == nil { + messageBusInfo.Optional = map[string]string{} + } + + // Since already validated, these are the only modes that can be set at this point. + switch messageBusInfo.AuthMode { + case AuthModeUsernamePassword: + messageBusInfo.Optional[OptionsUsernameKey] = secretData.Username + messageBusInfo.Optional[OptionsPasswordKey] = secretData.Password + case AuthModeCert: + messageBusInfo.Optional[OptionsCertPEMBlockKey] = string(secretData.CertPemBlock) + messageBusInfo.Optional[OptionsKeyPEMBlockKey] = string(secretData.KeyPemBlock) + case AuthModeCA: + messageBusInfo.Optional[OptionsCaPEMBlockKey] = string(secretData.CaPemBlock) + break + } + + return nil +} + +func GetSecretData(authMode string, secretName string, provider SecretDataProvider) (*SecretData, error) { + // No Auth? No Problem!...No secrets required. + if authMode == AuthModeNone { + return nil, nil + } + + secrets, err := provider.GetSecret(secretName) + if err != nil { + return nil, err + } + data := &SecretData{ + Username: secrets[SecretUsernameKey], + Password: secrets[SecretPasswordKey], + KeyPemBlock: []byte(secrets[SecretClientKey]), + CertPemBlock: []byte(secrets[SecretClientCert]), + CaPemBlock: []byte(secrets[SecretCACert]), + } + + return data, nil +} + +func ValidateSecretData(authMode string, secretName string, secretData *SecretData) error { + if authMode == AuthModeUsernamePassword { + if secretData.Username == "" || secretData.Password == "" { + return fmt.Errorf("AuthModeUsernamePassword selected however Username or Password was not found for secret=%s", secretName) + } + } else if authMode == AuthModeCert { + // need both to make a successful connection + if len(secretData.KeyPemBlock) <= 0 || len(secretData.CertPemBlock) <= 0 { + return fmt.Errorf("AuthModeCert selected however the key or cert PEM block was not found for secret=%s", secretName) + } + } else if authMode == AuthModeCA { + if len(secretData.CaPemBlock) <= 0 { + return fmt.Errorf("AuthModeCA selected however no PEM Block was found for secret=%s", secretName) + } + } else if authMode != AuthModeNone { + return fmt.Errorf("Invalid AuthMode selected") + } + + if len(secretData.CaPemBlock) > 0 { + caCertPool := x509.NewCertPool() + ok := caCertPool.AppendCertsFromPEM(secretData.CaPemBlock) + if !ok { + return errors.New("Error parsing CA Certificate") + } + } + + return nil +} diff --git a/bootstrap/messaging/messaging_test.go b/bootstrap/messaging/messaging_test.go new file mode 100644 index 00000000..69f6b830 --- /dev/null +++ b/bootstrap/messaging/messaging_test.go @@ -0,0 +1,354 @@ +package messaging + +import ( + "context" + "errors" + "os" + "sync" + "testing" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/config" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-messaging/v2/messaging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var lc logger.LoggingClient +var dic *di.Container +var usernameSecretData = map[string]string{ + SecretUsernameKey: "username", + SecretPasswordKey: "password", +} + +func TestMain(m *testing.M) { + lc = logger.NewMockClient() + + dic = di.NewContainer(di.ServiceConstructorMap{ + container.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + }) + + os.Exit(m.Run()) +} + +func TestBootstrapHandler(t *testing.T) { + validCreateClient := messageTestConfig{ + messageBusInfo: config.MessageBusInfo{ + Type: messaging.ZeroMQ, // Use ZMQ so no issue connecting. + Protocol: "http", + Host: "*", + Port: 8765, + PublishTopicPrefix: "edgex/events/#", + AuthMode: AuthModeUsernamePassword, + SecretName: "redisdb", + }, + } + + invalidSecrets := messageTestConfig{ + messageBusInfo: config.MessageBusInfo{ + AuthMode: AuthModeCert, + SecretName: "redisdb", + }, + } + + invalidNoConnect := messageTestConfig{ + messageBusInfo: config.MessageBusInfo{ + Type: messaging.MQTT, // This will cause no connection since broker not available + Protocol: "tcp", + Host: "localhost", + Port: 8765, + AuthMode: AuthModeUsernamePassword, + SecretName: "redisdb", + }, + } + + tests := []struct { + Name string + Config messageTestConfig + ExpectedResult bool + ExpectClient bool + }{ + {"Valid - creates client", validCreateClient, true, true}, + {"Invalid - secrets error", invalidSecrets, false, false}, + {"Invalid - can't connect", invalidNoConnect, false, false}, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + provider := &mocks.SecretProvider{} + provider.On("GetSecret", validCreateClient.GetMessageBusInfo().SecretName).Return(usernameSecretData, nil) + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationInterfaceName: func(get di.Get) interface{} { + return test.Config + }, + container.SecretProviderName: func(get di.Get) interface{} { + return provider + }, + container.MessagingClientName: func(get di.Get) interface{} { + return nil + }, + }) + + actual := BootstrapHandler(context.Background(), &sync.WaitGroup{}, startup.NewTimer(1, 1), dic) + assert.Equal(t, test.ExpectedResult, actual) + if test.ExpectClient { + assert.NotNil(t, container.MessagingClientFrom(dic.Get)) + } else { + assert.Nil(t, container.MessagingClientFrom(dic.Get)) + } + }) + } +} + +func TestGetSecretData(t *testing.T) { + // setup mock secret client + expectedSecretData := map[string]string{ + "username": "TEST_USER", + "password": "TEST_PASS", + } + + mockSecretProvider := &mocks.SecretProvider{} + mockSecretProvider.On("GetSecret", "").Return(nil) + mockSecretProvider.On("GetSecret", "notfound").Return(nil, errors.New("Not Found")) + mockSecretProvider.On("GetSecret", "mqtt").Return(expectedSecretData, nil) + + dic.Update(di.ServiceConstructorMap{ + container.SecretProviderName: func(get di.Get) interface{} { + return mockSecretProvider + }, + }) + + tests := []struct { + Name string + AuthMode string + SecretName string + ExpectedSecrets *SecretData + ExpectingError bool + }{ + {"No Auth No error", AuthModeNone, "", nil, false}, + {"Auth No SecretData found", AuthModeCA, "notfound", nil, true}, + {"Auth With SecretData", AuthModeUsernamePassword, "mqtt", &SecretData{ + Username: "TEST_USER", + Password: "TEST_PASS", + KeyPemBlock: []uint8{}, + CertPemBlock: []uint8{}, + CaPemBlock: []uint8{}, + }, false}, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + secretData, err := GetSecretData(test.AuthMode, test.SecretName, mockSecretProvider) + if test.ExpectingError { + assert.Error(t, err, "Expecting error") + return + } + require.Equal(t, test.ExpectedSecrets, secretData) + }) + } +} + +func TestValidateSecrets(t *testing.T) { + tests := []struct { + Name string + AuthMode string + secrets SecretData + ErrorExpectation bool + ErrorMessage string + }{ + {"Invalid AuthMode", "BadAuthMode", SecretData{}, true, "Invalid AuthMode selected"}, + {"No Auth No error", AuthModeNone, SecretData{}, false, ""}, + {"UsernamePassword No Error", AuthModeUsernamePassword, SecretData{ + Username: "user", + Password: "Password", + }, false, ""}, + {"UsernamePassword Error no Username", AuthModeUsernamePassword, SecretData{ + Password: "Password", + }, true, "AuthModeUsernamePassword selected however Username or Password was not found for secret=unit-test"}, + {"UsernamePassword Error no Password", AuthModeUsernamePassword, SecretData{ + Username: "user", + }, true, "AuthModeUsernamePassword selected however Username or Password was not found for secret=unit-test"}, + {"ClientCert No Error", AuthModeCert, SecretData{ + CertPemBlock: []byte("----"), + KeyPemBlock: []byte("----"), + }, false, ""}, + {"ClientCert No Key", AuthModeCert, SecretData{ + CertPemBlock: []byte("----"), + }, true, "AuthModeCert selected however the key or cert PEM block was not found for secret=unit-test"}, + {"ClientCert No Cert", AuthModeCert, SecretData{ + KeyPemBlock: []byte("----"), + }, true, "AuthModeCert selected however the key or cert PEM block was not found for secret=unit-test"}, + {"CACert no error", AuthModeCA, SecretData{ + CaPemBlock: []byte(testCACert), + }, false, ""}, + {"CACert invalid error", AuthModeCA, SecretData{ + CaPemBlock: []byte(`------`), + }, true, "Error parsing CA Certificate"}, + {"CACert no ca error", AuthModeCA, SecretData{}, true, "AuthModeCA selected however no PEM Block was found for secret=unit-test"}, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + result := ValidateSecretData(test.AuthMode, "unit-test", &test.secrets) + if test.ErrorExpectation { + assert.Error(t, result, "Result should be an error") + assert.Equal(t, test.ErrorMessage, result.(error).Error()) + } else { + assert.Nil(t, result, "Should be nil") + } + }) + } +} + +func TestSetOptionalAuthData(t *testing.T) { + tests := []struct { + Name string + Authmode string + SecretName string + Provider *mocks.SecretProvider + SecretData map[string]string + ExpectedOptionsData map[string]string + ErrorExpected bool + }{ + { + Name: "Valid Username/Password", + Authmode: AuthModeUsernamePassword, + SecretName: "user", + Provider: &mocks.SecretProvider{}, + SecretData: usernameSecretData, + ExpectedOptionsData: map[string]string{ + OptionsUsernameKey: "username", + OptionsPasswordKey: "password", + }, + ErrorExpected: false, + }, + { + Name: "Valid Client Cert", + Authmode: AuthModeCert, + SecretName: "client", + Provider: &mocks.SecretProvider{}, + SecretData: map[string]string{ + SecretClientCert: testClientCert, + SecretClientKey: testClientKey, + }, + ExpectedOptionsData: map[string]string{ + OptionsCertPEMBlockKey: testClientCert, + OptionsKeyPEMBlockKey: testClientKey, + }, + ErrorExpected: false, + }, + { + Name: "Valid CA Cert", + Authmode: AuthModeCA, + SecretName: "ca", + Provider: &mocks.SecretProvider{}, + SecretData: map[string]string{ + SecretCACert: testCACert, + }, + ExpectedOptionsData: map[string]string{ + OptionsCaPEMBlockKey: testCACert, + }, + ErrorExpected: false, + }, + { + Name: "Invalid - no provider", + Authmode: AuthModeUsernamePassword, + Provider: nil, + ErrorExpected: true, + }, + { + Name: "Invalid - Secret not found", + Authmode: AuthModeUsernamePassword, + SecretName: "", + Provider: &mocks.SecretProvider{}, + ErrorExpected: true, + }, + { + Name: "Invalid - Secret data invalid", + Authmode: AuthModeUsernamePassword, + SecretName: "user", + Provider: &mocks.SecretProvider{}, + SecretData: map[string]string{ + SecretCACert: testCACert, + }, + ErrorExpected: true, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + var dic *di.Container + if test.Provider != nil { + if len(test.SecretName) == 0 { + test.SecretName = "notfound" + test.Provider.On("GetSecret", test.SecretName).Return(nil, errors.New("Not Found")) + } else { + test.Provider.On("GetSecret", test.SecretName).Return(test.SecretData, nil) + } + + dic = di.NewContainer(di.ServiceConstructorMap{ + container.SecretProviderName: func(get di.Get) interface{} { + return test.Provider + }, + }) + } else { + dic = di.NewContainer(di.ServiceConstructorMap{}) + } + + messageBusInfo := config.MessageBusInfo{ + AuthMode: test.Authmode, + SecretName: test.SecretName, + } + + err := setOptionsAuthData(&messageBusInfo, lc, dic) + if test.ErrorExpected { + require.Error(t, err) + return + } + + assert.Equal(t, test.ExpectedOptionsData, messageBusInfo.Optional) + }) + } +} + +type messageTestConfig struct { + messageBusInfo config.MessageBusInfo +} + +func (c messageTestConfig) GetMessageBusInfo() config.MessageBusInfo { + return c.messageBusInfo +} + +func (c messageTestConfig) UpdateFromRaw(_ interface{}) bool { + panic("implement me") +} + +func (c messageTestConfig) UpdateWritableFromRaw(_ interface{}) bool { + panic("implement me") +} + +func (c messageTestConfig) EmptyWritablePtr() interface{} { + panic("implement me") +} + +func (c messageTestConfig) GetBootstrap() config.BootstrapConfiguration { + panic("implement me") +} + +func (c messageTestConfig) GetLogLevel() string { + panic("implement me") +} + +func (c messageTestConfig) GetRegistryInfo() config.RegistryInfo { + panic("implement me") +} + +func (c messageTestConfig) GetInsecureSecrets() config.InsecureSecrets { + panic("implement me") +} diff --git a/bootstrap/messaging/testcerts_test.go b/bootstrap/messaging/testcerts_test.go new file mode 100644 index 00000000..77028ce1 --- /dev/null +++ b/bootstrap/messaging/testcerts_test.go @@ -0,0 +1,89 @@ +// +// Copyright (c) 2021 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. +// + +package messaging + +const testCACert = `-----BEGIN CERTIFICATE----- +MIIDhTCCAm2gAwIBAgIUQl1RUGewZOXaSLnmH1i12zSYOtswDQYJKoZIhvcNAQEL +BQAwUjELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDELMAkGA1UEAwwCY2EwHhcNMjAwNDA4 +MDExNDQ2WhcNMjUwNDA4MDExNDQ2WjBSMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +U29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQsw +CQYDVQQDDAJjYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOqslFtX +nxr6yBZdLDKp1iTmsnFreEit7Z1BnNy9vQW6xrKRH+nxZWr0n9UIbx7KtmFkSBQ9 +Bb5zC/3ZdjcuQAuKSTgQB7AP1D2dX6geJPo1Ph9NS0aVmuUqQ6dU+/4R5ATfoWag +M7slCixfkBzbHEh0mCqr7FoDWq2h+Cz2n8K85tBZjLyUuzyRaqH7ZkHfJD1cxkGK +FcwudCg4zpKYOSctm+JpTlF6YPjlngN79jaJIQEAmx/twv1lOCAGBw/hZM3FGmQx +5dA1W7qaJ6NHgNRXWRS1AERtHpAAsWNBT1CKuAS/j0PlreRyR3aMgQYQ5camxi9a +qCrMiHybaqj+UCkCAwEAAaNTMFEwHQYDVR0OBBYEFPNCbvrfw2QDoOyYfNjT9sNO +52xOMB8GA1UdIwQYMBaAFPNCbvrfw2QDoOyYfNjT9sNO52xOMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHdFTqe6vi3BzgOMJEMO+81ZmiMohgKZ +Alyo8wH1C5RgwWW5w1OU+2RQfdOZgDfFkuQzmj0Kt2gzqACuAEtKzDt78lJ4f+WZ +MmRKBudJONUHTTm1micK3pqmn++nSygag0KxDvVbL+stSEgZwEBSOEvGDPXrL5qs +5yVOCi4xvsOCa1ymSnW6sX0z5GcgJQj2Znrr5QbEKHFSG86+WYEYnZ2zCNV7ahQo +bwXGZPOCUkpQzOstie/lPsf3Sd13/NIAk23TQ+rtaWIP9syQ85XWGRKRAUFOJEK0 +2/jr0Xot+Y/3raEfNSrq6sHTzX1q4PoWkSwNEEGXifBqDr+9PXK3mOQ= +-----END CERTIFICATE----- +` +const testClientCert = `-----BEGIN CERTIFICATE----- +MIIDLzCCAhcCFG+y+oEr87O2iQH90ayO4hU/GvSqMA0GCSqGSIb3DQEBCwUAMFIx +CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQxCzAJBgNVBAMMAmNhMB4XDTIwMDQwODAxMTY1 +OVoXDTIxMDQwMzAxMTY1OVowVjELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUt +U3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UE +AwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4TlobJoF +gNoCc5Znb0OzVoMypoay1RSTAhnU0arpHVugUMZMO6oxSt371MN+e4cUxoes4uhN +qeVG7AxUkdMCNJbzjAmJeDQtLKYHcY4YI30HHWCW0c8SxEsrj6DzjizgKZcUdX4H +6HwAltOp/RZYJTBVVexE1WYOheTNJuw5QeNbTGpfpKM7RuHADnytLbrSiK09FZYx +23PIsLhx8b7+k1AtRFGhFqDRMF6Fqbo6xdU8hZ1eAvJP5t87U/PWeQ9ld2lxd3fQ +xiP4IBQs1QI2gTp5O41ifRCpO7scXRaFweyPAgMVOQ42eVZiJUR37AF/nVzXxB5N +iTH9Ij/c/shJvQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDZ1tvo2JbA27qs+DzH +PQudMgCPqHylnqlbX94FtKrIh6kP4YwrMNoOCdcU/MHGG2b3ldoMgx9qrTnkk8g1 +3/gX/r4MDiTw2LocmIPYSukfR0J4k0ijlZtbtr9EtNPvy5iSla8Xi+iSm70wj+Zi +Z0GE0gOi8JfYPlxCtw3uVpsdqaHEevI70D4H1yAG22YYXUZt0QK02zztgBA2c7nE +kX0EMnYch0e7urs9o1M6JWJGlWZQxgVnxekbFDPfRelR1m0zFnbfXG2rnfuRpVEL +6SGxFU8+v1VepAHLvhS2VULYbWBOHZsh1yCteUXdePMYIN7c71qaCyC89N3GBia5 +uXOR +-----END CERTIFICATE----- +` +const testClientKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4TlobJoFgNoCc5Znb0OzVoMypoay1RSTAhnU0arpHVugUMZM +O6oxSt371MN+e4cUxoes4uhNqeVG7AxUkdMCNJbzjAmJeDQtLKYHcY4YI30HHWCW +0c8SxEsrj6DzjizgKZcUdX4H6HwAltOp/RZYJTBVVexE1WYOheTNJuw5QeNbTGpf +pKM7RuHADnytLbrSiK09FZYx23PIsLhx8b7+k1AtRFGhFqDRMF6Fqbo6xdU8hZ1e +AvJP5t87U/PWeQ9ld2lxd3fQxiP4IBQs1QI2gTp5O41ifRCpO7scXRaFweyPAgMV +OQ42eVZiJUR37AF/nVzXxB5NiTH9Ij/c/shJvQIDAQABAoIBADPL4BgZ0+ouOSIc +FO2hxDzBL4TctYQLl0OEbU1K4RG/YL8y25VdLrjpFGF6FDyUdFK0IS6N/k50TDs9 +GrXusTMnBBvQlazvUvRRuqSC6UpAFsLK0+SsmsRKBVqiyWCJMYRfGnVq5qaw3fHR +++YYnWzwELASBkKNlgl09TleWkysbnZbWIMQ5Qm0k+s/9vvjooA2aMXTeLtyhGfI +49OvyCrrX5v7ILdHl7RGAyPRT+ipyt1i0fAqHk4ouLdTRrAx4S5TvUpszrts1P8f +5ggLd1s6RVTz27uASu3U/gLH630m1PU46d02UI1tWen3TgRm/VqjO2aqkZaZispQ +HwTRZIECgYEA9rL7KoZflVQJ4ndg3V522BhAciN99taYWHr018kG5vNVGFBHSVOt +De0gb7z8FhK0Zs4MifU3b03qr7Ac1+p0zIAwATPT4TOLzc4SKBd33TZk/JCZCGSR +hqQPF0FZ+EKJqh7yif+ssFXp0xKrNybm58Z7jfF8vWMdz0QkJ1pZkn8CgYEA6bcp +YkH6IoHmCZ5hWE3/hYQcvfcM10z0cWTTKstxgSid9dj0HUqxMsFhBF1yzUtsDZQB +E933gZyj/LE5Z/EbqUSX0H/M0P7Uwtj9lS7W/vQdOQMfAciqggNKhyaBnBYsxw9l +5IelOxGF+taEvDkPsVt9cvZm/nbf+irU5JLCzcMCgYEA8o3/jUwY5oV+QoAFaSHb +z5PoqVBkJTHREA20dgVdF+3fmMw1is8Os0aWQcaaREmXvgyRH4NOQc1mFd8ePNx0 +giz3BfejNySrLGqUR37rh0BYAktZa3sV6j+b5s2GXCVvnShYZ35OmAGgqLsORGen +V/M6v9DTSJIPWR4yPc8DipkCgYEAhmtW/PFPaRtm7+9Ms5ogtWz3jvaRRx82lCVW +Io3iGVQADc8bD+HOqo94Oid5CMQxQFn4iLGoUb6Cvqo7hyGwNBmEa2GlripyuiJN +LslC1F4YlJrL8Z21G5PDAJpP/zLtzAt6Igc2LBP3B/7rVspG0U36h+1Z7U73oQ2T +ZmdWbTsCgYALxjB0NvqBk+TNYMZFysqZnI3CxYQXwHfElQQQUqcQnunAOLJ8H+nb +JryGx90ylYY2Mh2U273435uwQcX1g5gu3rBF8McHKj5EYSVDgpeBMx8ej2ENvW7q +CR6KVnoNdMwJZM3ARpBYNlhFTzDyew2WYLitZsN/uV8t+XxJFDyJQA== +-----END RSA PRIVATE KEY----- +` diff --git a/bootstrap/registration/registry_test.go b/bootstrap/registration/registry_test.go index 9bbece66..ffedce1f 100644 --- a/bootstrap/registration/registry_test.go +++ b/bootstrap/registration/registry_test.go @@ -64,6 +64,10 @@ type unitTestConfiguration struct { Registry config.RegistryInfo } +func (ut unitTestConfiguration) GetMessageBusInfo() config.MessageBusInfo { + panic("implement me") +} + func (ut unitTestConfiguration) GetInsecureSecrets() config.InsecureSecrets { return nil } diff --git a/bootstrap/secret/insecure.go b/bootstrap/secret/insecure.go index 0b05c4bd..53c0ade4 100644 --- a/bootstrap/secret/insecure.go +++ b/bootstrap/secret/insecure.go @@ -41,11 +41,11 @@ func NewInsecureProvider(config interfaces.Configuration, lc logger.LoggingClien } } -// GetSecrets retrieves secrets from a Insecure Secrets secret store. +// GetSecret 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) { +func (p *InsecureProvider) GetSecret(path string, keys ...string) (map[string]string, error) { results := make(map[string]string) pathExists := false var missingKeys []string @@ -92,8 +92,8 @@ func (p *InsecureProvider) GetSecrets(path string, keys ...string) (map[string]s return results, nil } -// StoreSecrets stores the secrets, but is not supported for Insecure Secrets -func (p *InsecureProvider) StoreSecrets(_ string, _ map[string]string) error { +// StoreSecret stores the secrets, but is not supported for Insecure Secrets +func (p *InsecureProvider) StoreSecret(_ string, _ map[string]string) error { return errors.New("storing secrets is not supported when running in insecure mode") } diff --git a/bootstrap/secret/insecure_test.go b/bootstrap/secret/insecure_test.go index 5829edf8..549cbd79 100644 --- a/bootstrap/secret/insecure_test.go +++ b/bootstrap/secret/insecure_test.go @@ -60,7 +60,7 @@ func TestInsecureProvider_GetSecrets(t *testing.T) { 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...) + actual, err := target.GetSecret(tc.Path, tc.Keys...) if tc.ExpectError { require.Error(t, err) return @@ -74,7 +74,7 @@ func TestInsecureProvider_GetSecrets(t *testing.T) { func TestInsecureProvider_StoreSecrets_Secure(t *testing.T) { target := NewInsecureProvider(nil, nil) - err := target.StoreSecrets("myPath", map[string]string{"Key": "value"}) + err := target.StoreSecret("myPath", map[string]string{"Key": "value"}) require.Error(t, err) } diff --git a/bootstrap/secret/secret_test.go b/bootstrap/secret/secret_test.go index cccc3ba9..b92b2a8f 100644 --- a/bootstrap/secret/secret_test.go +++ b/bootstrap/secret/secret_test.go @@ -117,7 +117,7 @@ func TestNewSecretProvider(t *testing.T) { actualProvider := container.SecretProviderFrom(dic.Get) assert.NotNil(t, actualProvider) - actualSecrets, err := actualProvider.GetSecrets(expectedPath) + actualSecrets, err := actualProvider.GetSecret(expectedPath) require.NoError(t, err) assert.Equal(t, expectedUsername, actualSecrets[UsernameKey]) assert.Equal(t, expectedPassword, actualSecrets[PasswordKey]) @@ -177,3 +177,7 @@ func (t TestConfig) GetRegistryInfo() bootstrapConfig.RegistryInfo { func (t TestConfig) GetInsecureSecrets() bootstrapConfig.InsecureSecrets { return t.InsecureSecrets } + +func (t TestConfig) GetMessageBusInfo() bootstrapConfig.MessageBusInfo { + panic("implement me") +} diff --git a/bootstrap/secret/secure.go b/bootstrap/secret/secure.go index 91626922..84c0a69b 100644 --- a/bootstrap/secret/secure.go +++ b/bootstrap/secret/secure.go @@ -59,11 +59,11 @@ func (p *SecureProvider) SetClient(client secrets.SecretClient) { p.secretClient = client } -// GetSecrets retrieves secrets from a secret store. +// GetSecret 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 *SecureProvider) GetSecrets(path string, keys ...string) (map[string]string, error) { +func (p *SecureProvider) GetSecret(path string, keys ...string) (map[string]string, error) { if cachedSecrets := p.getSecretsCache(path, keys...); cachedSecrets != nil { return cachedSecrets, nil } @@ -125,11 +125,11 @@ func (p *SecureProvider) updateSecretsCache(path string, secrets map[string]stri } } -// StoreSecrets stores the secrets to a secret store. +// StoreSecret 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 *SecureProvider) StoreSecrets(path string, secrets map[string]string) error { +func (p *SecureProvider) StoreSecret(path string, secrets map[string]string) error { if p.secretClient == nil { return errors.New("can't store secrets. Secure secret provider is not properly initialized") } diff --git a/bootstrap/secret/secure_test.go b/bootstrap/secret/secure_test.go index ff825739..eb72872a 100644 --- a/bootstrap/secret/secure_test.go +++ b/bootstrap/secret/secure_test.go @@ -57,7 +57,7 @@ func TestSecureProvider_GetSecrets(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { target := NewSecureProvider(tc.Config, logger.MockLogger{}, nil) target.SetClient(tc.Client) - actual, err := target.GetSecrets(tc.Path, tc.Keys...) + actual, err := target.GetSecret(tc.Path, tc.Keys...) if tc.ExpectError { require.Error(t, err) return @@ -79,19 +79,19 @@ func TestSecureProvider_GetSecrets_Cached(t *testing.T) { target := NewSecureProvider(nil, logger.MockLogger{}, nil) target.SetClient(mock) - actual, err := target.GetSecrets("redis", "username", "password") + actual, err := target.GetSecret("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") + actual, err = target.GetSecret("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")) - _, err = target.GetSecrets("redis", "username", "password2") + _, err = target.GetSecret("redis", "username", "password2") require.Error(t, err) } @@ -106,17 +106,17 @@ func TestSecureProvider_GetSecrets_Cached_Invalidated(t *testing.T) { target := NewSecureProvider(nil, logger.MockLogger{}, nil) target.SetClient(mock) - actual, err := target.GetSecrets("redis", "username", "password") + actual, err := target.GetSecret("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) + err = target.StoreSecret("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")) - _, err = target.GetSecrets("redis", "username", "password") + _, err = target.GetSecret("redis", "username", "password") require.Error(t, err) } @@ -143,7 +143,7 @@ func TestSecureProvider_StoreSecrets_Secure(t *testing.T) { target := NewSecureProvider(nil, logger.MockLogger{}, nil) target.SetClient(tc.Client) - err := target.StoreSecrets(tc.Path, input) + err := target.StoreSecret(tc.Path, input) if tc.ExpectError { require.Error(t, err) return @@ -164,7 +164,7 @@ func TestSecureProvider_SecretsLastUpdated(t *testing.T) { previous := target.SecretsLastUpdated() time.Sleep(1 * time.Second) - err := target.StoreSecrets("redis", input) + err := target.StoreSecret("redis", input) require.NoError(t, err) current := target.SecretsLastUpdated() assert.True(t, current.After(previous)) diff --git a/config/types.go b/config/types.go index 2a310b14..092d1d63 100644 --- a/config/types.go +++ b/config/types.go @@ -12,6 +12,7 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. *******************************************************************************/ + package config import ( @@ -150,3 +151,37 @@ type BootstrapConfiguration struct { Registry RegistryInfo SecretStore SecretStoreInfo } + +// MessageBusInfo provides parameters related to connecting to a message bus as a publisher +type MessageBusInfo struct { + // Indicates the message bus implementation to use, i.e. zero, mqtt, redisstreams... + Type string + // Protocol indicates the protocol to use when accessing the message bus. + Protocol string + // Host is the hostname or IP address of the broker, if applicable. + Host string + // Port defines the port on which to access the message bus. + Port int + // PublishTopicPrefix indicates the topic prefix the data is published to. + PublishTopicPrefix string + // SubscribeTopic indicates the topic in which to subscribe. + SubscribeTopic string + // AuthMode specifies the type of secure connection to the message bus which are 'none', 'usernamepassword' + // 'clientcert' or 'cacert'. Not all option supported by each implementation. + // ZMQ doesn't support any Authmode beyond 'none', RedisStreams only supports 'none' & 'usernamepassword' + // while MQTT supports all options. + AuthMode string + // SecretName is the name of the secret in the SecretStore that contains the Auth Credentials. The credential are + // dynamically loaded using this name and store the Option property below where the implementation expected to + // find them. + SecretName string + // Provides additional configuration properties which do not fit within the existing field. + // Typically the key is the name of the configuration property and the value is a string representation of the + // desired value for the configuration property. + Optional map[string]string +} + +// URL constructs a URL from the protocol, host and port and returns that as a string. +func (p MessageBusInfo) URL() string { + return fmt.Sprintf("%s://%s:%v", p.Protocol, p.Host, p.Port) +} diff --git a/go.mod b/go.mod index a931d1cc..2a2b145e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,8 @@ module github.com/edgexfoundry/go-mod-bootstrap/v2 require ( github.com/edgexfoundry/go-mod-configuration/v2 v2.0.0-dev.7 - github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.0-dev.78 + github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.0-dev.80 + github.com/edgexfoundry/go-mod-messaging/v2 v2.0.0-dev.11 github.com/edgexfoundry/go-mod-registry/v2 v2.0.0-dev.5 github.com/edgexfoundry/go-mod-secrets/v2 v2.0.0-dev.18 github.com/gorilla/mux v1.7.1