Skip to content

Commit

Permalink
enabling authentication for metric api scaler
Browse files Browse the repository at this point in the history
Signed-off-by: aman-bansal <bansalaman2905@gmail.com>
  • Loading branch information
aman-bansal committed Sep 10, 2020
1 parent d2ab1ad commit 2805ffc
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 0 additions & 33 deletions pkg/scalers/kafka_scaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package scalers

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strconv"
Expand Down Expand Up @@ -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 {
Expand Down
156 changes: 154 additions & 2 deletions pkg/scalers/metrics_api_scaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
53 changes: 53 additions & 0 deletions pkg/scalers/metrics_api_scaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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)
Expand Down Expand Up @@ -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")
}
}
}
}
38 changes: 38 additions & 0 deletions pkg/scalers/tls_config.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 2805ffc

Please sign in to comment.