diff --git a/appcontext/context.go b/appcontext/context.go index dc36b1377..2dea796c7 100644 --- a/appcontext/context.go +++ b/appcontext/context.go @@ -62,7 +62,7 @@ type Context struct { // RetryData holds the data to be stored for later retry when the pipeline function returns an error RetryData []byte // SecretProvider exposes the support for getting and storing secrets - SecretProvider *security.SecretProvider + SecretProvider security.SecretProvider } // Complete is optional and provides a way to return the specified data. diff --git a/appsdk/configupdates_test.go b/appsdk/configupdates_test.go index 3d26549fe..16e0a89ad 100644 --- a/appsdk/configupdates_test.go +++ b/appsdk/configupdates_test.go @@ -31,12 +31,13 @@ import ( ) func TestWaitForConfigUpdates_InsecureSecrets(t *testing.T) { - expected := time.Now() + _ = os.Setenv(security.EnvSecretStore, "false") + defer os.Clearenv() sdk := &AppFunctionsSDK{} - sdk.secretProvider = &security.SecretProvider{ - LastUpdated: expected, - } + sdk.secretProvider = &security.SecretProviderImpl{} + sdk.secretProvider.InsecureSecretsUpdated() + expected := sdk.secretProvider.SecretsLastUpdated() // Create all dependencies that are going to be required for this test sdk.LoggingClient = logger.NewMockClient() @@ -67,7 +68,7 @@ func TestWaitForConfigUpdates_InsecureSecrets(t *testing.T) { // Signal update occurred and give it time to process configUpdated <- struct{}{} time.Sleep(1 * time.Second) - assert.Equal(t, expected, sdk.secretProvider.LastUpdated, "LastUpdated should not have changed") + assert.Equal(t, expected, sdk.secretProvider.SecretsLastUpdated(), "LastUpdated should not have changed") // Add another new InsecureSecret entry so it is detected as changed. Must make a new map so // it doesn't add to the old map. @@ -83,5 +84,5 @@ func TestWaitForConfigUpdates_InsecureSecrets(t *testing.T) { // Signal update occurred and give it time to process configUpdated <- struct{}{} time.Sleep(1 * time.Second) - assert.NotEqual(t, expected, sdk.secretProvider.LastUpdated, "LastUpdated should have changed") + assert.NotEqual(t, expected, sdk.secretProvider.SecretsLastUpdated(), "LastUpdated should have changed") } diff --git a/appsdk/sdk.go b/appsdk/sdk.go index 501a77892..7a6a34848 100644 --- a/appsdk/sdk.go +++ b/appsdk/sdk.go @@ -103,7 +103,7 @@ type AppFunctionsSDK struct { registryClient registry.Client config *common.ConfigurationStruct storeClient interfaces.StoreClient - secretProvider *security.SecretProvider + secretProvider security.SecretProvider storeForwardWg *sync.WaitGroup storeForwardCancelCtx context.CancelFunc appWg *sync.WaitGroup diff --git a/go.mod b/go.mod index be44e1207..813765fbd 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/diegoholiveira/jsonlogic v1.0.1-0.20200220175622-ab7989be08b9 github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/edgexfoundry/go-mod-bootstrap v0.0.37 - github.com/edgexfoundry/go-mod-core-contracts v0.1.66 + github.com/edgexfoundry/go-mod-core-contracts v0.1.68 github.com/edgexfoundry/go-mod-messaging v0.1.21 github.com/edgexfoundry/go-mod-registry v0.1.21 github.com/edgexfoundry/go-mod-secrets v0.0.19 diff --git a/internal/bootstrap/container/secretprovider.go b/internal/bootstrap/container/secretprovider.go index 432e8b0e6..ec79ac65f 100644 --- a/internal/bootstrap/container/secretprovider.go +++ b/internal/bootstrap/container/secretprovider.go @@ -22,9 +22,9 @@ import ( ) // SecretProviderName contains the name of the security.SecretProvider implementation in the DIC. -var SecretProviderName = di.TypeInstanceToName(&security.SecretProvider{}) +var SecretProviderName = di.TypeInstanceToName((*security.SecretProvider)(nil)) // SecretProviderFrom helper function queries the DIC and returns the security.SecretProvider implementation. -func SecretProviderFrom(get di.Get) *security.SecretProvider { - return get(SecretProviderName).(*security.SecretProvider) +func SecretProviderFrom(get di.Get) security.SecretProvider { + return get(SecretProviderName).(security.SecretProvider) } diff --git a/internal/bootstrap/handlers/secrets.go b/internal/bootstrap/handlers/secrets.go index 6c53a36de..73bb1f5c2 100644 --- a/internal/bootstrap/handlers/secrets.go +++ b/internal/bootstrap/handlers/secrets.go @@ -46,7 +46,9 @@ func (_ *Secrets) BootstrapHandler( logger := bootstrapContainer.LoggingClientFrom(dic.Get) config := container.ConfigurationFrom(dic.Get) - secretProvider := security.NewSecretProvider(logger, config) + var secretProvider security.SecretProvider + + secretProvider = security.NewSecretProvider(logger, config) ok := secretProvider.Initialize(ctx) if !ok { logger.Error("unable to initialize secret provider") diff --git a/internal/bootstrap/handlers/storeclient.go b/internal/bootstrap/handlers/storeclient.go index 37044fed9..26a6a59e5 100644 --- a/internal/bootstrap/handlers/storeclient.go +++ b/internal/bootstrap/handlers/storeclient.go @@ -81,7 +81,7 @@ func (_ *Database) BootstrapHandler( // InitializeStoreClient initializes the database client for Store and Forward. This is not a receiver function so that // it can be called directly when configuration has changed and store and forward has been enabled for the first time func InitializeStoreClient( - secretProvider *security.SecretProvider, + secretProvider security.SecretProvider, config *common.ConfigurationStruct, startupTimer startup.Timer, logger logger.LoggingClient) (interfaces.StoreClient, error) { diff --git a/internal/constants.go b/internal/constants.go index 1462e8531..76668ebc3 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -22,11 +22,14 @@ import ( ) const ( - ConfigRegistryStem = "edgex/appservices/1.0/" - DatabaseName = "application-service" + ConfigRegistryStem = "edgex/appservices/1.0/" + DatabaseName = "application-service" + CorrelationHeaderKey = "X-Correlation-ID" ApiTriggerRoute = clients.ApiBase + "/trigger" ApiV2TriggerRoute = v2.ApiBase + "/trigger" + ApiSecretsRoute = clients.ApiBase + "/secrets" + ApiV2SecretsRoute = v2.ApiBase + "/secrets" ) // SDKVersion indicates the version of the SDK - will be overwritten by build @@ -34,6 +37,3 @@ var SDKVersion string = "0.0.0" // ApplicationVersion indicates the version of the application itself, not the SDK - will be overwritten by build var ApplicationVersion string = "0.0.0" - -// SecretsAPIRoute api route for posting secrets -var SecretsAPIRoute = clients.ApiBase + "/secrets" diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 731e3caa1..4f0e849eb 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -45,7 +45,7 @@ type GolangRuntime struct { transforms []appcontext.AppFunction isBusyCopying sync.Mutex storeForward storeForwardInfo - secretProvider *security.SecretProvider + secretProvider security.SecretProvider } type MessageError struct { @@ -139,7 +139,7 @@ func (gr *GolangRuntime) ProcessMessage(edgexcontext *appcontext.Context, envelo } // Initialize sets the internal reference to the StoreClient for use when Store and Forward is enabled -func (gr *GolangRuntime) Initialize(storeClient interfaces.StoreClient, secretProvider *security.SecretProvider) { +func (gr *GolangRuntime) Initialize(storeClient interfaces.StoreClient, secretProvider security.SecretProvider) { gr.storeForward.storeClient = storeClient gr.storeForward.runtime = gr gr.secretProvider = secretProvider diff --git a/internal/security/credentials.go b/internal/security/credentials.go index c7d8697d8..9fc13381c 100644 --- a/internal/security/credentials.go +++ b/internal/security/credentials.go @@ -26,7 +26,7 @@ import ( // GetDatabaseCredentials retrieves the login credentials for the database // If security is disabled then we use the insecure credentials supplied by the configuration. -func (s *SecretProvider) GetDatabaseCredentials(database db.DatabaseInfo) (common.Credentials, error) { +func (s *SecretProviderImpl) GetDatabaseCredentials(database db.DatabaseInfo) (common.Credentials, error) { var credentials map[string]string var err error @@ -63,7 +63,7 @@ func (s *SecretProvider) GetDatabaseCredentials(database db.DatabaseInfo) (commo // 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 (s *SecretProvider) GetSecrets(path string, keys ...string) (map[string]string, error) { +func (s *SecretProviderImpl) GetSecrets(path string, keys ...string) (map[string]string, error) { if !s.isSecurityEnabled() { return s.getInsecureSecrets(path, keys...) } @@ -89,7 +89,7 @@ func (s *SecretProvider) GetSecrets(path string, keys ...string) (map[string]str // 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 (s *SecretProvider) getInsecureSecrets(path string, keys ...string) (map[string]string, error) { +func (s *SecretProviderImpl) getInsecureSecrets(path string, keys ...string) (map[string]string, error) { secrets := make(map[string]string) pathExists := false var missingKeys []string @@ -130,7 +130,7 @@ func (s *SecretProvider) getInsecureSecrets(path string, keys ...string) (map[st return secrets, nil } -func (s *SecretProvider) getSecretsCache(path string, keys ...string) map[string]string { +func (s *SecretProviderImpl) getSecretsCache(path string, keys ...string) map[string]string { secrets := make(map[string]string) // Synchronize cache access @@ -159,7 +159,7 @@ func (s *SecretProvider) getSecretsCache(path string, keys ...string) map[string return nil } -func (s *SecretProvider) updateSecretsCache(path string, secrets map[string]string) { +func (s *SecretProviderImpl) updateSecretsCache(path string, secrets map[string]string) { // Synchronize cache access s.cacheMuxtex.Lock() defer s.cacheMuxtex.Unlock() @@ -177,7 +177,7 @@ func (s *SecretProvider) updateSecretsCache(path string, secrets map[string]stri // 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 (s *SecretProvider) StoreSecrets(path string, secrets map[string]string) error { +func (s *SecretProviderImpl) StoreSecrets(path string, secrets map[string]string) error { if !s.isSecurityEnabled() { return errors.New("Storing secrets is not supported when running in insecure mode") } diff --git a/internal/security/credentials_test.go b/internal/security/credentials_test.go index 3b893c332..d3483f76a 100644 --- a/internal/security/credentials_test.go +++ b/internal/security/credentials_test.go @@ -151,7 +151,6 @@ func getSecretsTestData() secretTestData { } func TestGetSecrets(t *testing.T) { - secretProvider := newMockSecretProvider(nil) for i, test := range getSecretsTestData() { @@ -189,7 +188,7 @@ func TestGetInsecureSecrets(t *testing.T) { tearDownGetInsecureSecrets(t, origEnv) } -func setupGetInsecureSecrets(t *testing.T) (sp *SecretProvider, origEnv string) { +func setupGetInsecureSecrets(t *testing.T) (sp *SecretProviderImpl, origEnv string) { insecureSecrets := common.InsecureSecrets{ "no_path": common.InsecureSecretsInfo{ Path: "", @@ -230,7 +229,7 @@ func tearDownGetInsecureSecrets(t *testing.T, origEnv string) { } } -func newMockSecretProvider(configuration *common.ConfigurationStruct) *SecretProvider { +func newMockSecretProvider(configuration *common.ConfigurationStruct) *SecretProviderImpl { logClient := logger.NewClient("app_functions_sdk_go", false, "./test.log", "DEBUG") mockSP := NewSecretProvider(logClient, configuration) mockSP.SharedSecretClient = &mockSecretClient{} diff --git a/internal/security/secret.go b/internal/security/secret.go index e4cdfb77f..93b245e65 100644 --- a/internal/security/secret.go +++ b/internal/security/secret.go @@ -28,6 +28,7 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/internal/security/authtokenloader" "github.com/edgexfoundry/app-functions-sdk-go/internal/security/client" "github.com/edgexfoundry/app-functions-sdk-go/internal/security/fileioperformer" + "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" @@ -37,8 +38,17 @@ import ( const EnvSecretStore = "EDGEX_SECURITY_SECRET_STORE" +type SecretProvider interface { + Initialize(_ context.Context) bool + StoreSecrets(path string, secrets map[string]string) error + GetSecrets(path string, _ ...string) (map[string]string, error) + GetDatabaseCredentials(database db.DatabaseInfo) (common.Credentials, error) + InsecureSecretsUpdated() + SecretsLastUpdated() time.Time +} + // SecretProvider cache storage for the secrets -type SecretProvider struct { +type SecretProviderImpl struct { SharedSecretClient pkg.SecretClient ExclusiveSecretClient pkg.SecretClient secretsCache map[string]map[string]string // secret's path, key, value @@ -50,8 +60,8 @@ type SecretProvider struct { } // NewSecretProvider returns a new secret provider -func NewSecretProvider(loggingClient logger.LoggingClient, configuration *common.ConfigurationStruct) *SecretProvider { - sp := &SecretProvider{ +func NewSecretProvider(loggingClient logger.LoggingClient, configuration *common.ConfigurationStruct) *SecretProviderImpl { + sp := &SecretProviderImpl{ secretsCache: make(map[string]map[string]string), cacheMuxtex: &sync.Mutex{}, configuration: configuration, @@ -63,7 +73,7 @@ func NewSecretProvider(loggingClient logger.LoggingClient, configuration *common } // Initialize creates SecretClients to be used for obtaining secrets from a secrets store manager. -func (s *SecretProvider) Initialize(ctx context.Context) bool { +func (s *SecretProviderImpl) Initialize(ctx context.Context) bool { var err error // initialize shared secret client if configured @@ -83,13 +93,17 @@ func (s *SecretProvider) Initialize(ctx context.Context) bool { // InsecureSecretsUpdated resets LastUpdate is not running in secure mode.If running in secure mode, changes to // InsecureSecrets have no impact and are not used. -func (s *SecretProvider) InsecureSecretsUpdated() { +func (s *SecretProviderImpl) InsecureSecretsUpdated() { if !s.isSecurityEnabled() { s.LastUpdated = time.Now() } } -func (s *SecretProvider) initializeSecretClient( +func (s *SecretProviderImpl) SecretsLastUpdated() time.Time { + return s.LastUpdated +} + +func (s *SecretProviderImpl) initializeSecretClient( ctx context.Context, secretStoreInfo bootstrapConfig.SecretStoreInfo) (pkg.SecretClient, error) { var secretClient pkg.SecretClient @@ -140,7 +154,7 @@ func (s *SecretProvider) initializeSecretClient( // getSecretConfig creates a SecretConfig based on the SecretStoreInfo configuration properties. // If a tokenfile is present it will override the Authentication.AuthToken value. // the return boolean is used to indicate whether the secret store configuration is empty or not -func (s *SecretProvider) getSecretConfig(secretStoreInfo bootstrapConfig.SecretStoreInfo) (vault.SecretConfig, bool, error) { +func (s *SecretProviderImpl) getSecretConfig(secretStoreInfo bootstrapConfig.SecretStoreInfo) (vault.SecretConfig, bool, error) { emptySecretStore := bootstrapConfig.SecretStoreInfo{} if secretStoreInfo == emptySecretStore { return vault.SecretConfig{}, true, nil @@ -178,7 +192,7 @@ func (s *SecretProvider) getSecretConfig(secretStoreInfo bootstrapConfig.SecretS } // isSecurityEnabled determines if security has been enabled. -func (s *SecretProvider) isSecurityEnabled() bool { +func (s *SecretProviderImpl) isSecurityEnabled() bool { env := os.Getenv(EnvSecretStore) return env != "false" } diff --git a/internal/security/secret_test.go b/internal/security/secret_test.go index 11e426598..f552f24c5 100644 --- a/internal/security/secret_test.go +++ b/internal/security/secret_test.go @@ -195,11 +195,12 @@ func TestInitializeClientFromSecretProvider(t *testing.T) { func TestInsecureSecretsUpdated(t *testing.T) { expected := time.Now() - target := SecretProvider{ + target := SecretProviderImpl{ LastUpdated: expected, } os.Setenv(EnvSecretStore, "true") + target.InsecureSecretsUpdated() assert.Equal(t, expected, target.LastUpdated, "LastUpdated should not have changed") diff --git a/internal/trigger/http/rest.go b/internal/trigger/http/rest.go index 4e6fba490..5766ac203 100644 --- a/internal/trigger/http/rest.go +++ b/internal/trigger/http/rest.go @@ -71,7 +71,7 @@ func (trigger *Trigger) requestHandler(writer http.ResponseWriter, r *http.Reque logger.Debug("Request Body read", "byte count", len(data)) - correlationID := r.Header.Get("X-Correlation-ID") + correlationID := r.Header.Get(internal.CorrelationHeaderKey) edgexContext := &appcontext.Context{ CorrelationID: correlationID, Configuration: trigger.Configuration, diff --git a/internal/v2/controller/http/controller.go b/internal/v2/controller/http/controller.go index 38ff8d9ad..2a4142184 100644 --- a/internal/v2/controller/http/controller.go +++ b/internal/v2/controller/http/controller.go @@ -20,57 +20,80 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "github.com/edgexfoundry/go-mod-core-contracts/clients" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" contractsV2 "github.com/edgexfoundry/go-mod-core-contracts/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" - "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/edgexfoundry/app-functions-sdk-go/internal" sdkCommon "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/internal/security" "github.com/edgexfoundry/app-functions-sdk-go/internal/telemetry" - - "github.com/edgexfoundry/app-functions-sdk-go/internal" + "github.com/edgexfoundry/app-functions-sdk-go/internal/v2/dtos/requests" ) -// V2Controller controller for V2 REST APIs -type V2Controller struct { - lc logger.LoggingClient - config *sdkCommon.ConfigurationStruct +// V2HttpController controller for V2 REST APIs +type V2HttpController struct { + router *mux.Router + secretProvider security.SecretProvider + lc logger.LoggingClient + config *sdkCommon.ConfigurationStruct } -// NewV2Controller creates and initializes an V2Controller -func NewV2Controller(lc logger.LoggingClient, config *sdkCommon.ConfigurationStruct) *V2Controller { - return &V2Controller{ - lc: lc, - config: config, +// NewV2HttpController creates and initializes an V2HttpController +func NewV2HttpController( + router *mux.Router, + lc logger.LoggingClient, + config *sdkCommon.ConfigurationStruct, + secretProvider security.SecretProvider) *V2HttpController { + return &V2HttpController{ + router: router, + secretProvider: secretProvider, + lc: lc, + config: config, } } +// ConfigureStandardRoutes loads standard V2 routes +func (v2c *V2HttpController) ConfigureStandardRoutes() { + v2c.lc.Info("Registering standard V2 routes...") + v2c.router.HandleFunc(contractsV2.ApiPingRoute, v2c.Ping).Methods(http.MethodGet) + v2c.router.HandleFunc(contractsV2.ApiVersionRoute, v2c.Version).Methods(http.MethodGet) + v2c.router.HandleFunc(contractsV2.ApiMetricsRoute, v2c.Metrics).Methods(http.MethodGet) + v2c.router.HandleFunc(contractsV2.ApiConfigRoute, v2c.Config).Methods(http.MethodGet) + v2c.router.HandleFunc(internal.ApiV2SecretsRoute, v2c.Secrets).Methods(http.MethodPost) + + /// V2 Trigger is not considered a standard route. Trigger route (when configured) is setup by the HTTP Trigger + // in internal/trigger/http/rest.go +} + // Ping handles the request to /ping endpoint. Is used to test if the service is working // It returns a response as specified by the V2 API swagger in openapi/v2 -func (v2c *V2Controller) Ping(w http.ResponseWriter, _ *http.Request) { +func (v2c *V2HttpController) Ping(writer http.ResponseWriter, request *http.Request) { response := common.NewPingResponse() - v2c.sendResponse(w, contractsV2.ApiPingRoute, response, uuid.New().String()) + v2c.sendResponse(writer, request, contractsV2.ApiPingRoute, response, http.StatusOK) } // Version handles the request to /version endpoint. Is used to request the service's versions // It returns a response as specified by the V2 API swagger in openapi/v2 -func (v2c *V2Controller) Version(w http.ResponseWriter, _ *http.Request) { +func (v2c *V2HttpController) Version(writer http.ResponseWriter, request *http.Request) { response := common.NewVersionSdkResponse(internal.ApplicationVersion, internal.SDKVersion) - v2c.sendResponse(w, contractsV2.ApiVersionRoute, response, uuid.New().String()) + v2c.sendResponse(writer, request, contractsV2.ApiVersionRoute, response, http.StatusOK) } // Config handles the request to /config endpoint. Is used to request the service's configuration // It returns a response as specified by the V2 API swagger in openapi/v2 -func (v2c *V2Controller) Config(w http.ResponseWriter, _ *http.Request) { +func (v2c *V2HttpController) Config(writer http.ResponseWriter, request *http.Request) { response := common.NewConfigResponse(*v2c.config) - v2c.sendResponse(w, contractsV2.ApiVersionRoute, response, uuid.New().String()) + v2c.sendResponse(writer, request, contractsV2.ApiVersionRoute, response, http.StatusOK) } // Metrics handles the request to the /metrics endpoint, memory and cpu utilization stats // It returns a response as specified by the V2 API swagger in openapi/v2 -func (v2c *V2Controller) Metrics(w http.ResponseWriter, r *http.Request) { +func (v2c *V2HttpController) Metrics(writer http.ResponseWriter, request *http.Request) { telem := telemetry.NewSystemUsage() metrics := common.Metrics{ MemAlloc: telem.Memory.Alloc, @@ -83,29 +106,81 @@ func (v2c *V2Controller) Metrics(w http.ResponseWriter, r *http.Request) { } response := common.NewMetricsResponse(metrics) - v2c.sendResponse(w, contractsV2.ApiMetricsRoute, response, uuid.New().String()) + v2c.sendResponse(writer, request, contractsV2.ApiMetricsRoute, response, http.StatusOK) +} + +// Secrets handles the request to add App Service exclusive secrets to the Secret Store +// It returns a response as specified by the V2 API swagger in openapi/v2 +func (v2c *V2HttpController) Secrets(writer http.ResponseWriter, request *http.Request) { + defer func() { + _ = request.Body.Close() + }() + + secretRequest := requests.SecretsRequest{} + err := json.NewDecoder(request.Body).Decode(&secretRequest) + if err != nil { + response := common.NewBaseResponse("unknown", err.Error(), http.StatusBadRequest) + v2c.sendResponse(writer, request, internal.ApiV2SecretsRoute, response, http.StatusMultiStatus) + return + } + + path, secrets := v2c.prepareSecrets(secretRequest) + + if err := v2c.secretProvider.StoreSecrets(path, secrets); err != nil { + msg := fmt.Sprintf("Storing secrets failed: %v", err) + response := common.NewBaseResponse(secretRequest.RequestID, msg, http.StatusInternalServerError) + v2c.sendResponse(writer, request, internal.ApiV2SecretsRoute, response, http.StatusMultiStatus) + return + } + + response := common.NewBaseResponseNoMessage(secretRequest.RequestID, http.StatusCreated) + v2c.sendResponse(writer, request, internal.ApiV2SecretsRoute, response, http.StatusMultiStatus) } // sendResponse puts together the response packet for the V2 API -// api is the V2 API path -// item is the object or data that is sent back as part of the response -// correlationID is a unique identifier correlating a request to its associated response -func (v2c *V2Controller) sendResponse(w http.ResponseWriter, api string, item interface{}, correlationID string) { - data, err := json.Marshal(item) +func (v2c *V2HttpController) sendResponse( + writer http.ResponseWriter, + request *http.Request, + api string, + response interface{}, + statusCode int) { + + correlationID := request.Header.Get(internal.CorrelationHeaderKey) + + writer.WriteHeader(statusCode) + writer.Header().Set(internal.CorrelationHeaderKey, correlationID) + writer.Header().Set(clients.ContentType, clients.ContentTypeJSON) + + data, err := json.Marshal(response) if err != nil { v2c.lc.Error(fmt.Sprintf("Unable to marshal %s response", api), "error", err.Error(), clients.CorrelationHeader, correlationID) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(writer, err.Error(), http.StatusInternalServerError) return } - _, err = w.Write(data) + _, err = writer.Write(data) if err != nil { v2c.lc.Error(fmt.Sprintf("Unable to write %s response", api), "error", err.Error(), clients.CorrelationHeader, correlationID) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(writer, err.Error(), http.StatusInternalServerError) return } +} + +func (v2c *V2HttpController) prepareSecrets(request requests.SecretsRequest) (string, map[string]string) { + var secretsKV = make(map[string]string) + for _, secret := range request.Secrets { + secretsKV[secret.Key] = secret.Value + } + + path := strings.TrimSpace(request.Path) + + // add '/' in the full URL path if it's not already at the end of the basepath or subpath + if !strings.HasSuffix(v2c.config.SecretStoreExclusive.Path, "/") && !strings.HasPrefix(path, "/") { + path = "/" + path + } else if strings.HasSuffix(v2c.config.SecretStoreExclusive.Path, "/") && strings.HasPrefix(path, "/") { + // remove extra '/' in the full URL path because secret store's (Vault) APIs don't handle extra '/'. + path = path[1:] + } - w.Header().Set(clients.CorrelationHeader, correlationID) - w.Header().Set(clients.ContentType, clients.ContentTypeJSON) - w.WriteHeader(http.StatusOK) + return path, secretsKV } diff --git a/internal/v2/controller/http/controller_test.go b/internal/v2/controller/http/controller_test.go index c6e7064c9..4784eb35c 100644 --- a/internal/v2/controller/http/controller_test.go +++ b/internal/v2/controller/http/controller_test.go @@ -18,8 +18,11 @@ package http import ( "encoding/json" + "io" "net/http" "net/http/httptest" + "os" + "strings" "testing" "time" @@ -28,17 +31,61 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" contractsV2 "github.com/edgexfoundry/go-mod-core-contracts/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/google/uuid" + "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/edgexfoundry/app-functions-sdk-go/internal" sdkCommon "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/internal/security" + "github.com/edgexfoundry/app-functions-sdk-go/internal/v2/dtos/requests" ) +var expectedCorrelationId = uuid.New().String() + +func TestConfigureStandardRoutes(t *testing.T) { + router := mux.NewRouter() + target := NewV2HttpController(router, logger.NewMockClient(), nil, nil) + target.ConfigureStandardRoutes() + + var routes []*mux.Route + walkFunc := func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + routes = append(routes, route) + return nil + } + err := router.Walk(walkFunc) + + require.NoError(t, err) + assert.Len(t, routes, 5) + + paths := make(map[string]string) + for _, route := range routes { + url, err := route.URLPath("", "") + require.NoError(t, err) + methods, err := route.GetMethods() + require.NoError(t, err) + require.Len(t, methods, 1) + paths[url.Path] = methods[0] + } + + assert.Contains(t, paths, contractsV2.ApiPingRoute) + assert.Contains(t, paths, contractsV2.ApiConfigRoute) + assert.Contains(t, paths, contractsV2.ApiMetricsRoute) + assert.Contains(t, paths, contractsV2.ApiVersionRoute) + assert.Contains(t, paths, internal.ApiV2SecretsRoute) + + assert.Equal(t, http.MethodGet, paths[contractsV2.ApiPingRoute]) + assert.Equal(t, http.MethodGet, paths[contractsV2.ApiConfigRoute]) + assert.Equal(t, http.MethodGet, paths[contractsV2.ApiMetricsRoute]) + assert.Equal(t, http.MethodGet, paths[contractsV2.ApiVersionRoute]) + assert.Equal(t, http.MethodPost, paths[internal.ApiV2SecretsRoute]) +} + func TestPingRequest(t *testing.T) { - target := NewV2Controller(logger.NewMockClient(), nil) + target := NewV2HttpController(nil, logger.NewMockClient(), nil, nil) - recorder := doGetRequest(t, contractsV2.ApiPingRoute, target.Ping) + recorder := doRequest(t, http.MethodGet, contractsV2.ApiPingRoute, target.Ping, nil) actual := common.PingResponse{} err := json.Unmarshal(recorder.Body.Bytes(), &actual) @@ -57,9 +104,9 @@ func TestVersionRequest(t *testing.T) { internal.ApplicationVersion = expectedAppVersion internal.SDKVersion = expectedSdkVersion - target := NewV2Controller(logger.NewMockClient(), nil) + target := NewV2HttpController(nil, logger.NewMockClient(), nil, nil) - recorder := doGetRequest(t, contractsV2.ApiVersion, target.Version) + recorder := doRequest(t, http.MethodGet, contractsV2.ApiVersion, target.Version, nil) actual := common.VersionSdkResponse{} err := json.Unmarshal(recorder.Body.Bytes(), &actual) @@ -71,9 +118,9 @@ func TestVersionRequest(t *testing.T) { } func TestMetricsRequest(t *testing.T) { - target := NewV2Controller(logger.NewMockClient(), nil) + target := NewV2HttpController(nil, logger.NewMockClient(), nil, nil) - recorder := doGetRequest(t, contractsV2.ApiMetricsRoute, target.Metrics) + recorder := doRequest(t, http.MethodGet, contractsV2.ApiMetricsRoute, target.Metrics, nil) actual := common.MetricsResponse{} err := json.Unmarshal(recorder.Body.Bytes(), &actual) @@ -101,9 +148,9 @@ func TestConfigRequest(t *testing.T) { }, } - target := NewV2Controller(logger.NewMockClient(), &expectedConfig) + target := NewV2HttpController(nil, logger.NewMockClient(), &expectedConfig, nil) - recorder := doGetRequest(t, contractsV2.ApiConfigRoute, target.Config) + recorder := doRequest(t, http.MethodGet, contractsV2.ApiConfigRoute, target.Config, nil) actualResponse := common.ConfigResponse{} err := json.Unmarshal(recorder.Body.Bytes(), &actualResponse) @@ -123,20 +170,120 @@ func TestConfigRequest(t *testing.T) { assert.Equal(t, expectedConfig, actualConfig) } -func doGetRequest(t *testing.T, api string, handler http.HandlerFunc) *httptest.ResponseRecorder { - req, err := http.NewRequest(http.MethodGet, api, nil) +func TestSecretsRequest(t *testing.T) { + expectedRequestId := "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc" + config := &sdkCommon.ConfigurationStruct{ + SecretStoreExclusive: bootstrapConfig.SecretStoreInfo{ + Path: "TBD", + }, + } + lc := logger.NewMockClient() + + mockProvider := NewSecretProviderMock(config) + target := NewV2HttpController(nil, lc, config, mockProvider) + assert.NotNil(t, target) + + validRequest := requests.SecretsRequest{ + BaseRequest: common.BaseRequest{RequestID: expectedRequestId}, + Path: "mqtt", + Secrets: []requests.SecretsKeyValue{ + {Key: "username", Value: "username"}, + {Key: "password", Value: "password"}, + }, + } + + validNoPath := validRequest + validNoPath.Path = "" + validPathWithSlash := validRequest + validPathWithSlash.Path = "/mqtt" + noRequestId := validRequest + noRequestId.RequestID = "" + noSecrets := validRequest + noSecrets.Secrets = []requests.SecretsKeyValue{} + missingSecretKey := validRequest + missingSecretKey.Secrets = []requests.SecretsKeyValue{ + {Key: "", Value: "username"}, + } + missingSecretValue := validRequest + missingSecretValue.Secrets = []requests.SecretsKeyValue{ + {Key: "username", Value: ""}, + } + + tests := []struct { + Name string + Request requests.SecretsRequest + SecretsPath string + SecretStoreEnabled string + ErrorExpected bool + ExpectedStatusCode int + }{ + {"Valid - sub-path no trailing slash, SecretsPath has trailing slash", validRequest, "my-secrets/", "true", false, http.StatusCreated}, + {"Valid - no trailing slashes", validNoPath, "my-secrets", "true", false, http.StatusCreated}, + {"Valid - sub-path only with trailing slash", validPathWithSlash, "my-secrets", "true", false, http.StatusCreated}, + {"Valid - both trailing slashes", validPathWithSlash, "my-secrets/", "true", false, http.StatusCreated}, + {"Invalid - no requestId", noRequestId, "", "true", true, http.StatusBadRequest}, + {"Invalid - no secrets", noSecrets, "", "true", true, http.StatusBadRequest}, + {"Invalid - missing secret key", missingSecretKey, "", "true", true, http.StatusBadRequest}, + {"Invalid - missing secret value", missingSecretValue, "", "true", true, http.StatusBadRequest}, + {"Invalid - No Secret Store", validRequest, "", "false", true, http.StatusInternalServerError}, + } + + for _, testCase := range tests { + t.Run(testCase.Name, func(t *testing.T) { + os.Setenv(security.EnvSecretStore, testCase.SecretStoreEnabled) + + jsonData, err := json.Marshal(testCase.Request) + require.NoError(t, err) + + reader := strings.NewReader(string(jsonData)) + + req, err := http.NewRequest(http.MethodPost, internal.ApiV2SecretsRoute, reader) + require.NoError(t, err) + req.Header.Set(internal.CorrelationHeaderKey, expectedCorrelationId) + + config.SecretStoreExclusive.Path = testCase.SecretsPath + + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(target.Secrets) + handler.ServeHTTP(recorder, req) + + actualResponse := common.BaseResponse{} + err = json.Unmarshal(recorder.Body.Bytes(), &actualResponse) + require.NoError(t, err) + + assert.Equal(t, contractsV2.ApiVersion, actualResponse.ApiVersion, "Api Version not as expected") + assert.Equal(t, testCase.ExpectedStatusCode, int(actualResponse.StatusCode), "Response status code not as expected") + + if testCase.ErrorExpected { + assert.NotEmpty(t, actualResponse.Message, "Message is empty") + return // Test complete for error cases + } + + assert.Equal(t, expectedRequestId, actualResponse.RequestID, "RequestID not as expected") + assert.Empty(t, actualResponse.Message, "Message not empty, as expected") + }) + } +} + +func doRequest(t *testing.T, method string, api string, handler http.HandlerFunc, body io.Reader) *httptest.ResponseRecorder { + req, err := http.NewRequest(method, api, body) require.NoError(t, err) + req.Header.Set(internal.CorrelationHeaderKey, expectedCorrelationId) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) - require.Equal(t, http.StatusOK, recorder.Code) + expectedStatusCode := http.StatusOK + if method == http.MethodPost { + expectedStatusCode = http.StatusMultiStatus + } - assert.Equal(t, clients.ContentTypeJSON, recorder.HeaderMap.Get(clients.ContentType)) - assert.NotEmpty(t, recorder.HeaderMap.Get(clients.CorrelationHeader)) + assert.Equal(t, expectedStatusCode, recorder.Code, "Wrong status code") + assert.Equal(t, clients.ContentTypeJSON, recorder.HeaderMap.Get(clients.ContentType), "Content type not set or not JSON") + assert.Equal(t, expectedCorrelationId, recorder.HeaderMap.Get(internal.CorrelationHeaderKey), "CorrelationHeader not as expected") - require.NotEmpty(t, recorder.Body.String()) + require.NotEmpty(t, recorder.Body.String(), "Response body is empty") return recorder } diff --git a/internal/v2/controller/http/mockprovider_test.go b/internal/v2/controller/http/mockprovider_test.go new file mode 100644 index 000000000..f20df1853 --- /dev/null +++ b/internal/v2/controller/http/mockprovider_test.go @@ -0,0 +1,104 @@ +// +// Copyright (c) 2020 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 http + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/internal/security" + "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db" +) + +type SecretProviderMock struct { + config *common.ConfigurationStruct + mockSecretStore map[string]map[string]string // secret's path, key, value + + //used to track when secrets have last been retrieved + secretsLastUpdated time.Time +} + +// NewSecretProviderMock returns a new mock secret provider +func NewSecretProviderMock(config *common.ConfigurationStruct) *SecretProviderMock { + sp := &SecretProviderMock{} + sp.config = config + sp.mockSecretStore = make(map[string]map[string]string) + return sp +} + +// Initialize does nothing. +func (s *SecretProviderMock) Initialize(_ context.Context) bool { + return true +} + +// StoreSecrets saves secrets to the mock secret store. +func (s *SecretProviderMock) StoreSecrets(path string, secrets map[string]string) error { + testFullPath := s.config.SecretStoreExclusive.Path + path + // Base path should not have any leading slashes, only trailing or none, for this test to work + if strings.Contains(testFullPath, "//") || !strings.Contains(testFullPath, "/") { + return fmt.Errorf("Path is malformed: path=%s", path) + } + + if !s.isSecurityEnabled() { + return fmt.Errorf("Storing secrets is not supported when running in insecure mode") + } + s.mockSecretStore[path] = secrets + return nil +} + +// GetSecrets retrieves secrets from a mock secret store. +func (s *SecretProviderMock) GetSecrets(path string, _ ...string) (map[string]string, error) { + secrets, ok := s.mockSecretStore[path] + if !ok { + return nil, fmt.Errorf("no secrets for path '%s' found", path) + } + return secrets, nil +} + +// GetDatabaseCredentials retrieves the login credentials for the database from mock secret store +func (s *SecretProviderMock) GetDatabaseCredentials(database db.DatabaseInfo) (common.Credentials, error) { + credentials, ok := s.mockSecretStore[database.Type] + if !ok { + return common.Credentials{}, fmt.Errorf("no credentials for type '%s' found", database.Type) + } + + return common.Credentials{ + Username: credentials["username"], + Password: credentials["password"], + }, nil +} + +// InsecureSecretsUpdated resets LastUpdate is not running in secure mode.If running in secure mode, changes to +// InsecureSecrets have no impact and are not used. +func (s *SecretProviderMock) InsecureSecretsUpdated() { + s.secretsLastUpdated = time.Now() +} + +// SecretsLastUpdated returns the time stamp when the provider secrets cache was latest updated +func (s *SecretProviderMock) SecretsLastUpdated() time.Time { + return s.secretsLastUpdated +} + +// isSecurityEnabled determines if security has been enabled. +func (s *SecretProviderMock) isSecurityEnabled() bool { + env := os.Getenv(security.EnvSecretStore) + return env != "false" +} diff --git a/internal/v2/dtos/requests/secrets.go b/internal/v2/dtos/requests/secrets.go new file mode 100644 index 000000000..83d91912a --- /dev/null +++ b/internal/v2/dtos/requests/secrets.go @@ -0,0 +1,66 @@ +// +// Copyright (c) 2020 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 requests + +import ( + "encoding/json" + + v2 "github.com/edgexfoundry/go-mod-core-contracts/v2" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" +) + +// SecretsKeyValue is a secret key/value pair to be stored in the Secret Store +// See detail specified by the V2 API swagger in openapi/v2 +type SecretsKeyValue struct { + Key string `json:"key" validate:"required"` + Value string `json:"value" validate:"required"` +} + +// SecretsRequest is the request DTO for storing supplied secrets at specified Path in the Secret Store +// See detail specified by the V2 API swagger in openapi/v2 +type SecretsRequest struct { + common.BaseRequest `json:",inline"` + Path string `json:"path"` + Secrets []SecretsKeyValue `json:"secrets" validate:"required,gt=0,dive"` +} + +// Validate satisfies the Validator interface +func (sr SecretsRequest) Validate() error { + err := v2.Validate(sr) + return err +} + +// UnmarshalJSON implements the Unmarshaler interface for the SecretsRequest type +func (sr *SecretsRequest) UnmarshalJSON(b []byte) error { + var alias struct { + common.BaseRequest + Path string + Secrets []SecretsKeyValue + } + + if err := json.Unmarshal(b, &alias); err != nil { + return v2.NewErrContractInvalid("Failed to unmarshal request body as JSON.") + } + + *sr = SecretsRequest(alias) + + // validate SecretsRequest DTO + if err := sr.Validate(); err != nil { + return err + } + return nil +} diff --git a/internal/v2/dtos/requests/secrets_test.go b/internal/v2/dtos/requests/secrets_test.go new file mode 100644 index 000000000..9204e1c1f --- /dev/null +++ b/internal/v2/dtos/requests/secrets_test.go @@ -0,0 +1,110 @@ +// +// Copyright (c) 2020 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 requests + +import ( + "encoding/json" + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + TestUUID = "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc" +) + +var validRequest = SecretsRequest{ + BaseRequest: common.BaseRequest{RequestID: TestUUID}, + Path: "", + Secrets: []SecretsKeyValue{ + {Key: "password", Value: "password"}, + }, +} + +var missingKeySecrets = []SecretsKeyValue{ + {Key: "", Value: "password"}, +} + +var missingValueSecrets = []SecretsKeyValue{ + {Key: "password", Value: ""}, +} + +func TestSecretsRequest_Validate(t *testing.T) { + validNoPath := validRequest + validWithPath := validRequest + validWithPath.Path = "mqtt" + noRequestId := validRequest + noRequestId.RequestID = "" + noSecrets := validRequest + noSecrets.Secrets = []SecretsKeyValue{} + missingSecretKey := validRequest + missingSecretKey.Secrets = missingKeySecrets + missingSecretValue := validRequest + missingSecretValue.Secrets = missingValueSecrets + + tests := []struct { + Name string + Request SecretsRequest + ErrorExpected bool + }{ + {"valid with no path", validNoPath, false}, + {"valid with with path", validWithPath, false}, + + {"invalid no requestId", noRequestId, true}, + {"invalid no Secrets", noSecrets, true}, + {"invalid missing secret key", missingSecretKey, true}, + {"invalid missing secret value", missingSecretValue, true}, + } + for _, testCase := range tests { + t.Run(testCase.Name, func(t *testing.T) { + err := testCase.Request.Validate() + assert.Equal(t, testCase.ErrorExpected, err != nil, "Unexpected addDeviceRequest validation result.", err) + }) + } +} + +func TestSecretsRequest_UnmarshalJSON(t *testing.T) { + resultTestBytes, _ := json.Marshal(validRequest) + + tests := []struct { + Name string + Expected SecretsRequest + Data []byte + ErrorExpected bool + }{ + {"unmarshal with success", validRequest, resultTestBytes, false}, + {"unmarshal invalid, empty data", SecretsRequest{}, []byte{}, true}, + {"unmarshal invalid, non-json data", SecretsRequest{}, []byte("Invalid SecretsRequest"), true}, + } + + for _, testCase := range tests { + t.Run(testCase.Name, func(t *testing.T) { + actual := SecretsRequest{} + err := actual.UnmarshalJSON(testCase.Data) + if testCase.ErrorExpected { + require.Error(t, err) + return // Test complete + } + + require.NoError(t, err) + assert.Equal(t, testCase.Expected, actual, "Unmarshal did not result in expected SecretsRequest.") + + }) + } +} diff --git a/internal/v2/routes.go b/internal/v2/routes.go deleted file mode 100644 index 160af24c9..000000000 --- a/internal/v2/routes.go +++ /dev/null @@ -1,33 +0,0 @@ -// -// 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 v2 - -import ( - "net/http" - - "github.com/edgexfoundry/app-functions-sdk-go/internal/common" - v2http "github.com/edgexfoundry/app-functions-sdk-go/internal/v2/controller/http" - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - contractsV2 "github.com/edgexfoundry/go-mod-core-contracts/v2" - "github.com/gorilla/mux" -) - -// ConfigureStandardRoutes loads standard V2 routes -func ConfigureStandardRoutes(router *mux.Router, lc logger.LoggingClient, config *common.ConfigurationStruct) { - controller := v2http.NewV2Controller(lc, config) - - lc.Info("Registering standard V2 routes...") - - router.HandleFunc(contractsV2.ApiPingRoute, controller.Ping).Methods(http.MethodGet) - router.HandleFunc(contractsV2.ApiVersionRoute, controller.Version).Methods(http.MethodGet) - router.HandleFunc(contractsV2.ApiMetricsRoute, controller.Metrics).Methods(http.MethodGet) - router.HandleFunc(contractsV2.ApiConfigRoute, controller.Config).Methods(http.MethodGet) -} diff --git a/internal/webserver/server.go b/internal/webserver/server.go index 9369779d4..cb0101058 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -27,7 +27,8 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/internal/security" "github.com/edgexfoundry/app-functions-sdk-go/internal/telemetry" - v2 "github.com/edgexfoundry/app-functions-sdk-go/internal/v2" + v2 "github.com/edgexfoundry/app-functions-sdk-go/internal/v2/controller/http" + "github.com/edgexfoundry/go-mod-core-contracts/clients" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" @@ -36,10 +37,11 @@ import ( // WebServer handles the webserver configuration type WebServer struct { - Config *common.ConfigurationStruct - LoggingClient logger.LoggingClient - router *mux.Router - secretProvider *security.SecretProvider + Config *common.ConfigurationStruct + LoggingClient logger.LoggingClient + router *mux.Router + secretProvider security.SecretProvider + v2HttpController *v2.V2HttpController } // swagger:model @@ -49,12 +51,13 @@ type Version struct { } // NewWebserver returns a new instance of *WebServer -func NewWebServer(config *common.ConfigurationStruct, secretProvider *security.SecretProvider, lc logger.LoggingClient, router *mux.Router) *WebServer { +func NewWebServer(config *common.ConfigurationStruct, secretProvider security.SecretProvider, lc logger.LoggingClient, router *mux.Router) *WebServer { ws := &WebServer{ - Config: config, - LoggingClient: lc, - router: router, - secretProvider: secretProvider, + Config: config, + LoggingClient: lc, + router: router, + secretProvider: secretProvider, + v2HttpController: v2.NewV2HttpController(router, lc, config, secretProvider), } return ws @@ -293,10 +296,10 @@ func (webserver *WebServer) ConfigureStandardRoutes() { webserver.router.HandleFunc(clients.ApiVersionRoute, webserver.versionHandler).Methods(http.MethodGet) // Secrets - webserver.router.HandleFunc(internal.SecretsAPIRoute, webserver.secretHandler).Methods(http.MethodPost) + webserver.router.HandleFunc(internal.ApiSecretsRoute, webserver.secretHandler).Methods(http.MethodPost) // V2 API routes - v2.ConfigureStandardRoutes(webserver.router, webserver.LoggingClient, webserver.Config) + webserver.v2HttpController.ConfigureStandardRoutes() } // SetupTriggerRoute adds a route to handle trigger pipeline from HTTP request diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index 09d9e57fa..f1e3b704b 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -196,7 +196,7 @@ func TestPostSecretRoute(t *testing.T) { for _, test := range tests { currentTest := test t.Run(test.name, func(t *testing.T) { - req, _ := http.NewRequest(http.MethodPost, internal.SecretsAPIRoute, bytes.NewReader(currentTest.payload)) + req, _ := http.NewRequest(http.MethodPost, internal.ApiSecretsRoute, bytes.NewReader(currentTest.payload)) rr := httptest.NewRecorder() webserver.router.ServeHTTP(rr, req) assert.Equal(t, currentTest.expectedStatus, rr.Result().StatusCode, "Expected secret doesn't match postSecret") @@ -220,7 +220,7 @@ type mockSecretClient struct { } // NewMockSecretProvider provides a mocked version of the mockSecretClient to avoiding using vault in our tests -func newMockSecretProvider(loggingClient logger.LoggingClient, configuration *common.ConfigurationStruct) *security.SecretProvider { +func newMockSecretProvider(loggingClient logger.LoggingClient, configuration *common.ConfigurationStruct) security.SecretProvider { mockSP := security.NewSecretProvider(logClient, config) return mockSP } diff --git a/pkg/transforms/http_test.go b/pkg/transforms/http_test.go index 69ab46344..d17fc242f 100644 --- a/pkg/transforms/http_test.go +++ b/pkg/transforms/http_test.go @@ -186,7 +186,7 @@ type mockSecretClient struct { } // NewMockSecretProvider provides a mocked version of the mockSecretClient to avoiding using vault in our tests -func newMockSecretProvider(loggingClient logger.LoggingClient, configuration *common.ConfigurationStruct) *security.SecretProvider { +func newMockSecretProvider(loggingClient logger.LoggingClient, configuration *common.ConfigurationStruct) security.SecretProvider { mockSP := security.NewSecretProvider(logClient, config) mockSP.ExclusiveSecretClient = &mockSecretClient{} return mockSP diff --git a/pkg/transforms/mqttsecret.go b/pkg/transforms/mqttsecret.go index 8504dd846..7cb2a9f64 100644 --- a/pkg/transforms/mqttsecret.go +++ b/pkg/transforms/mqttsecret.go @@ -237,7 +237,7 @@ func (sender *MQTTSecretSender) MQTTSend(edgexcontext *appcontext.Context, param return false, err } // if we havent initialized the client yet OR the cache has been invalidated (due to new/updated secrets) we need to (re)initialize the client - if sender.client == nil || sender.secretsLastRetrieved.Before(edgexcontext.SecretProvider.LastUpdated) { + if sender.client == nil || sender.secretsLastRetrieved.Before(edgexcontext.SecretProvider.SecretsLastUpdated()) { err := sender.initializeMQTTClient(edgexcontext) if err != nil { return false, err