From 2805ffcc927747894f542281b9873528ee3fadd5 Mon Sep 17 00:00:00 2001 From: aman-bansal Date: Thu, 10 Sep 2020 17:49:54 +0530 Subject: [PATCH] enabling authentication for metric api scaler Signed-off-by: aman-bansal --- CHANGELOG.md | 1 + pkg/scalers/kafka_scaler.go | 33 ------ pkg/scalers/metrics_api_scaler.go | 156 ++++++++++++++++++++++++- pkg/scalers/metrics_api_scaler_test.go | 53 +++++++++ pkg/scalers/tls_config.go | 38 ++++++ 5 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 pkg/scalers/tls_config.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 496b74521df..a00e8ed88b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - Added ScaledObject Status Conditions to display status of scaling ([#750](https://github.com/kedacore/keda/pull/750)) - Added optional authentication parameters for the Redis Scaler ([#962](https://github.com/kedacore/keda/pull/962)) - Improved GCP PubSub Scaler performance by closing the client correctly ([#1087](https://github.com/kedacore/keda/pull/1087)) +- Enable support for authentication in metrics api scaler ([#1137](https://github.com/kedacore/keda/pull/1137)) ### Breaking Changes diff --git a/pkg/scalers/kafka_scaler.go b/pkg/scalers/kafka_scaler.go index a8a0846ee2e..df388f19fda 100644 --- a/pkg/scalers/kafka_scaler.go +++ b/pkg/scalers/kafka_scaler.go @@ -2,8 +2,6 @@ package scalers import ( "context" - "crypto/tls" - "crypto/x509" "errors" "fmt" "strconv" @@ -255,37 +253,6 @@ func getKafkaClients(metadata kafkaMetadata) (sarama.Client, sarama.ClusterAdmin return client, admin, nil } -// newTLSConfig returns a *tls.Config using the given ceClient cert, ceClient key, -// and CA certificate. If none are appropriate, a nil *tls.Config is returned. -func newTLSConfig(clientCert, clientKey, caCert string) (*tls.Config, error) { - valid := false - - config := &tls.Config{} - - if clientCert != "" && clientKey != "" { - cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) - if err != nil { - return nil, fmt.Errorf("error parse X509KeyPair: %s", err) - } - config.Certificates = []tls.Certificate{cert} - valid = true - } - - if caCert != "" { - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM([]byte(caCert)) - config.RootCAs = caCertPool - config.InsecureSkipVerify = true - valid = true - } - - if !valid { - config = nil - } - - return config, nil -} - func (s *kafkaScaler) getPartitions() ([]int32, error) { topicsMetadata, err := s.admin.DescribeTopics([]string{s.metadata.topic}) if err != nil { diff --git a/pkg/scalers/metrics_api_scaler.go b/pkg/scalers/metrics_api_scaler.go index 0d3b795fed6..a4dfc524828 100644 --- a/pkg/scalers/metrics_api_scaler.go +++ b/pkg/scalers/metrics_api_scaler.go @@ -15,18 +15,56 @@ import ( "net/http" logf "sigs.k8s.io/controller-runtime/pkg/log" "strconv" + "strings" + "time" ) type metricsAPIScaler struct { metadata *metricsAPIScalerMetadata + client *http.Client } type metricsAPIScalerMetadata struct { targetValue int url string valueLocation string + + //apiKeyAuth + enableAPIKeyAuth bool + // +default is header + method method + // +option default header key is X-API-KEY and default query key is api_key + keyParamName string + apiKey string + + //base auth + enableBaseAuth bool + username string + // +optional + password string + + //client certification + enableTLS bool + cert string + key string + ca string } +type authenticationType string + +const ( + apiKeyAuth authenticationType = "apiKeyAuth" + basicAuth = "basicAuth" + tlsAuth = "tlsAuth" +) + +type method string + +const ( + header method = "header" + queryParam = "query" +) + var httpLog = logf.Log.WithName("metrics_api_scaler") // NewMetricsAPIScaler creates a new HTTP scaler @@ -35,7 +73,24 @@ func NewMetricsAPIScaler(resolvedEnv, metadata, authParams map[string]string) (S if err != nil { return nil, fmt.Errorf("error parsing metric API metadata: %s", err) } - return &metricsAPIScaler{metadata: meta}, nil + + var transport *http.Transport + if meta.enableTLS { + config, err := newTLSConfig(meta.cert, meta.key, meta.ca) + if err != nil { + return nil, err + } + + transport = &http.Transport{TLSClientConfig: config} + } + + return &metricsAPIScaler{ + metadata: meta, + client: &http.Client{ + Timeout: 3 * time.Second, + Transport: transport, + }, + }, nil } func metricsAPIMetadata(resolvedEnv, metadata, authParams map[string]string) (*metricsAPIScalerMetadata, error) { @@ -63,6 +118,56 @@ func metricsAPIMetadata(resolvedEnv, metadata, authParams map[string]string) (*m return nil, fmt.Errorf("no valueLocation given in metadata") } + if val, ok := authParams["authMode"]; ok { + val = strings.TrimSpace(val) + authType := authenticationType(val) + if authType == apiKeyAuth { + if len(authParams["apiKey"]) == 0 { + return nil, errors.New("no apikey provided") + } + + meta.apiKey = authParams["apiKey"] + meta.method = header + meta.enableAPIKeyAuth = true + + if authParams["method"] == queryParam { + meta.method = queryParam + } + + if len(authParams["keyParamName"]) > 0 { + meta.keyParamName = authParams["keyParamName"] + } + } else if authType == basicAuth { + if authParams["username"] == "" { + return nil, errors.New("no username given") + } + + meta.username = authParams["username"] + meta.password = authParams["password"] + meta.enableBaseAuth = true + + } else if authType == tlsAuth { + if authParams["ca"] == "" { + return nil, errors.New("no ca given") + } + + meta.ca = authParams["ca"] + if authParams["cert"] == "" { + return nil, errors.New("no cert given") + } + + meta.cert = authParams["cert"] + if authParams["key"] == "" { + return nil, errors.New("no key given") + } + + meta.key = authParams["key"] + meta.enableTLS = true + } else { + return nil, fmt.Errorf("err incorrect value for authMode is given: %s", val) + } + } + return &meta, nil } @@ -77,7 +182,12 @@ func GetValueFromResponse(body []byte, valueLocation string) (int64, error) { } func (s *metricsAPIScaler) getMetricValue() (int64, error) { - r, err := http.Get(s.metadata.url) + request, err := getAuthenticatedRequest(s.metadata) + if err != nil { + return 0, err + } + + r, err := s.client.Do(request) if err != nil { return 0, err } @@ -149,3 +259,45 @@ func (s *metricsAPIScaler) GetMetrics(ctx context.Context, metricName string, me return append([]external_metrics.ExternalMetricValue{}, metric), nil } + +func getAuthenticatedRequest(meta *metricsAPIScalerMetadata) (*http.Request, error) { + var req *http.Request + var err error + + if meta.enableAPIKeyAuth { + if header == meta.method { + req, err = http.NewRequest("GET", meta.url, nil) + if err != nil { + return nil, err + } + + if len(meta.keyParamName) == 0 { + req.Header.Add("X-API-KEY", meta.apiKey) + } else { + req.Header.Add(meta.keyParamName, meta.apiKey) + } + + } else if queryParam == meta.method { + var url string + if len(meta.keyParamName) == 0 { + url = url + "?api_key=" + meta.apiKey + } else { + url = url + "?" + meta.keyParamName + "=" + meta.apiKey + } + + req, err = http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + } + } else if meta.enableBaseAuth { + req, err = http.NewRequest("GET", meta.url, nil) + if err != nil { + return nil, err + } + + req.SetBasicAuth(meta.username, meta.password) + } + + return req, nil +} diff --git a/pkg/scalers/metrics_api_scaler_test.go b/pkg/scalers/metrics_api_scaler_test.go index c9a736ae3ab..6484baa0ee0 100644 --- a/pkg/scalers/metrics_api_scaler_test.go +++ b/pkg/scalers/metrics_api_scaler_test.go @@ -12,6 +12,8 @@ type metricsAPIMetadataTestData struct { raisesError bool } +var validMetricAPIMetadata = map[string]string{"url": "http://dummy:1230/api/v1/", "valueLocation": "metric", "targetValue": "42"} + var testMetricsAPIMetadata = []metricsAPIMetadataTestData{ // No metadata {metadata: map[string]string{}, raisesError: true}, @@ -27,6 +29,36 @@ var testMetricsAPIMetadata = []metricsAPIMetadataTestData{ {metadata: map[string]string{"url": "http://dummy:1230/api/v1/", "valueLocation": "metric"}, raisesError: true}, } +type metricAPIAuthMetadataTestData struct { + authParams map[string]string + isError bool +} + +var testMetricsAPIAuthMetadata = []metricAPIAuthMetadataTestData{ + // success TLS + {map[string]string{"authMode": "tlsAuth", "ca": "caaa", "cert": "ceert", "key": "keey"}, false}, + // fail TLS, ca not given + {map[string]string{"authMode": "tlsAuth", "cert": "ceert", "key": "keey"}, true}, + // fail TLS, key not given + {map[string]string{"authMode": "tlsAuth", "ca": "caaa", "cert": "ceert"}, true}, + // fail TLS, cert not given + {map[string]string{"authMode": "tlsAuth", "ca": "caaa", "key": "keey"}, true}, + // success apiKeyAuth default + {map[string]string{"authMode": "apiKeyAuth", "apiKey": "apiikey"}, false}, + // success apiKeyAuth as query param + {map[string]string{"authMode": "apiKeyAuth", "apiKey": "apiikey", "method": "query"}, false}, + // success apiKeyAuth with headers and custom key name + {map[string]string{"authMode": "apiKeyAuth", "apiKey": "apiikey", "method": "header", "keyParamName": "custom"}, false}, + // success apiKeyAuth with query param and custom key name + {map[string]string{"authMode": "apiKeyAuth", "apiKey": "apiikey", "method": "query", "keyParamName": "custom"}, false}, + // fail apiKeyAuth with no api key + {map[string]string{"authMode": "apiKeyAuth"}, true}, + // success basicAuth + {map[string]string{"authMode": "basicAuth", "username": "user", "password": "pass"}, false}, + // fail basicAuth with no username + {map[string]string{"authMode": "basicAuth"}, true}, +} + func TestParseMetricsAPIMetadata(t *testing.T) { for _, testData := range testMetricsAPIMetadata { _, err := metricsAPIMetadata(metricsAPIResolvedEnv, testData.metadata, authParams) @@ -58,3 +90,24 @@ func TestGetValueFromResponse(t *testing.T) { } } + +func TestMetricAPIScalerAuthParams(t *testing.T) { + for _, testData := range testMetricsAPIAuthMetadata { + meta, err := metricsAPIMetadata(nil, validMetricAPIMetadata, testData.authParams) + + if err != nil && !testData.isError { + t.Error("Expected success but got error", err) + } + if testData.isError && err == nil { + t.Error("Expected error but got success") + } + + if err == nil { + if (meta.enableAPIKeyAuth && !(testData.authParams["authMode"] == "apiKeyAuth")) || + (meta.enableBaseAuth && !(testData.authParams["authMode"] == "basicAuth")) || + (meta.enableTLS && !(testData.authParams["authMode"] == "tlsAuth")) { + t.Error("wrong auth mode detected") + } + } + } +} diff --git a/pkg/scalers/tls_config.go b/pkg/scalers/tls_config.go new file mode 100644 index 00000000000..78a5cc2dcef --- /dev/null +++ b/pkg/scalers/tls_config.go @@ -0,0 +1,38 @@ +package scalers + +import ( + "crypto/tls" + "crypto/x509" + "fmt" +) + +// newTLSConfig returns a *tls.Config using the given ceClient cert, ceClient key, +// and CA certificate. If none are appropriate, a nil *tls.Config is returned. +func newTLSConfig(clientCert, clientKey, caCert string) (*tls.Config, error) { + valid := false + + config := &tls.Config{} + + if clientCert != "" && clientKey != "" { + cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) + if err != nil { + return nil, fmt.Errorf("error parse X509KeyPair: %s", err) + } + config.Certificates = []tls.Certificate{cert} + valid = true + } + + if caCert != "" { + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM([]byte(caCert)) + config.RootCAs = caCertPool + config.InsecureSkipVerify = true + valid = true + } + + if !valid { + config = nil + } + + return config, nil +}