diff --git a/go.mod b/go.mod index 283a376c..acc62ffd 100644 --- a/go.mod +++ b/go.mod @@ -116,14 +116,14 @@ require ( go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/crypto v0.28.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect diff --git a/go.sum b/go.sum index 6fa95c33..28ca84d2 100644 --- a/go.sum +++ b/go.sum @@ -322,8 +322,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -342,8 +342,8 @@ golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -357,14 +357,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= diff --git a/pkg/httpclient/httpclient.go b/pkg/httpclient/httpclient.go new file mode 100644 index 00000000..1a38d174 --- /dev/null +++ b/pkg/httpclient/httpclient.go @@ -0,0 +1,219 @@ +package httpclient + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/grafana/grafana-aws-sdk/pkg/awsds" + "github.com/grafana/grafana-aws-sdk/pkg/sigv4" + "github.com/grafana/grafana-infinity-datasource/pkg/models" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" + "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" + "github.com/icholy/digest" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + "golang.org/x/oauth2/jwt" +) + +func GetHTTPClient(ctx context.Context, settings models.InfinitySettings) (*http.Client, error) { + httpClient, err := getBaseHTTPClient(ctx, settings) + if httpClient == nil { + return httpClient, errors.Join(models.ErrCreatingHTTPClient, err) + } + httpClient, err = applyDigestAuth(ctx, httpClient, settings) + if err != nil { + return httpClient, errors.Join(models.ErrCreatingHTTPClient, err) + } + httpClient, err = applyOAuthClientCredentials(ctx, httpClient, settings) + if err != nil { + return httpClient, errors.Join(models.ErrCreatingHTTPClient, err) + } + httpClient, err = applyOAuthJWT(ctx, httpClient, settings) + if err != nil { + return httpClient, errors.Join(models.ErrCreatingHTTPClient, err) + } + httpClient, err = applyAWSAuth(ctx, httpClient, settings) + if err != nil { + return httpClient, errors.Join(models.ErrCreatingHTTPClient, err) + } + httpClient, err = applySecureSocksProxyConfiguration(ctx, httpClient, settings) + if err != nil { + return httpClient, errors.Join(models.ErrCreatingHTTPClient, err) + } + return httpClient, nil +} + +func getBaseHTTPClient(ctx context.Context, settings models.InfinitySettings) (*http.Client, error) { + logger := backend.Logger.FromContext(ctx) + tlsConfig, err := GetTLSConfigFromSettings(settings) + if err != nil { + return nil, err + } + transport := &http.Transport{TLSClientConfig: tlsConfig} + switch settings.ProxyType { + case models.ProxyTypeNone: + logger.Debug("proxy type is set to none. Not using the proxy") + case models.ProxyTypeUrl: + logger.Debug("proxy type is set to url. Using the proxy", "proxy_url", settings.ProxyUrl) + u, err := url.Parse(settings.ProxyUrl) + if err != nil { + logger.Error("error parsing proxy url", "err", err.Error(), "proxy_url", settings.ProxyUrl) + return nil, err + } + transport.Proxy = http.ProxyURL(u) + default: + transport.Proxy = http.ProxyFromEnvironment + } + + return &http.Client{ + Transport: transport, + Timeout: time.Second * time.Duration(settings.TimeoutInSeconds), + }, nil +} + +func isDigestAuthConfigured(settings models.InfinitySettings) bool { + return settings.AuthenticationMethod == models.AuthenticationMethodDigestAuth +} + +func applyDigestAuth(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { + _, span := tracing.DefaultTracer().Start(ctx, "ApplyDigestAuth") + defer span.End() + if isDigestAuthConfigured(settings) { + a := digest.Transport{Username: settings.UserName, Password: settings.Password, Transport: httpClient.Transport} + httpClient.Transport = &a + } + return httpClient, nil +} + +func isOAuthCredentialsConfigured(settings models.InfinitySettings) bool { + return settings.AuthenticationMethod == models.AuthenticationMethodOAuth && settings.OAuth2Settings.OAuth2Type == models.AuthOAuthTypeClientCredentials +} + +func applyOAuthClientCredentials(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { + _, span := tracing.DefaultTracer().Start(ctx, "ApplyOAuthClientCredentials") + defer span.End() + if isOAuthCredentialsConfigured(settings) { + oauthConfig := clientcredentials.Config{ + ClientID: settings.OAuth2Settings.ClientID, + ClientSecret: settings.OAuth2Settings.ClientSecret, + TokenURL: settings.OAuth2Settings.TokenURL, + Scopes: []string{}, + EndpointParams: url.Values{}, + AuthStyle: settings.OAuth2Settings.AuthStyle, + } + for _, scope := range settings.OAuth2Settings.Scopes { + if scope != "" { + oauthConfig.Scopes = append(oauthConfig.Scopes, scope) + } + } + for k, v := range settings.OAuth2Settings.EndpointParams { + if k != "" && v != "" { + oauthConfig.EndpointParams.Set(k, v) + } + } + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) + httpClient = oauthConfig.Client(ctx) + } + return httpClient, nil +} + +func isOAuthJWTConfigured(settings models.InfinitySettings) bool { + return settings.AuthenticationMethod == models.AuthenticationMethodOAuth && settings.OAuth2Settings.OAuth2Type == models.AuthOAuthJWT +} + +func applyOAuthJWT(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { + _, span := tracing.DefaultTracer().Start(ctx, "ApplyOAuthJWT") + defer span.End() + if isOAuthJWTConfigured(settings) { + jwtConfig := jwt.Config{ + Email: settings.OAuth2Settings.Email, + TokenURL: settings.OAuth2Settings.TokenURL, + PrivateKey: []byte(strings.ReplaceAll(settings.OAuth2Settings.PrivateKey, "\\n", "\n")), + PrivateKeyID: settings.OAuth2Settings.PrivateKeyID, + Subject: settings.OAuth2Settings.Subject, + Scopes: []string{}, + } + for _, scope := range settings.OAuth2Settings.Scopes { + if scope != "" { + jwtConfig.Scopes = append(jwtConfig.Scopes, scope) + } + } + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) + httpClient = jwtConfig.Client(ctx) + } + return httpClient, nil +} + +func isAwsAuthConfigured(settings models.InfinitySettings) bool { + return settings.AuthenticationMethod == models.AuthenticationMethodAWS +} + +func applyAWSAuth(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { + ctx, span := tracing.DefaultTracer().Start(ctx, "ApplyAWSAuth") + defer span.End() + if isAwsAuthConfigured(settings) { + tempHttpClient, err := getBaseHTTPClient(ctx, settings) + if err != nil { + return tempHttpClient, err + } + authType := settings.AWSSettings.AuthType + if authType == "" { + authType = models.AWSAuthTypeKeys + } + region := settings.AWSSettings.Region + if region == "" { + region = "us-east-2" + } + service := settings.AWSSettings.Service + if service == "" { + service = "monitoring" + } + conf := &sigv4.Config{ + AuthType: string(authType), + Region: region, + Service: service, + AccessKey: settings.AWSAccessKey, + SecretKey: settings.AWSSecretKey, + } + + authSettings := awsds.ReadAuthSettings(ctx) + rt, err := sigv4.New(conf, *authSettings, sigv4.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + req.Header.Add("Accept", "application/json") + return tempHttpClient.Do(req) + })) + if err != nil { + return httpClient, err + } + httpClient.Transport = rt + } + return httpClient, nil +} + +func applySecureSocksProxyConfiguration(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { + logger := backend.Logger.FromContext(ctx) + if isAwsAuthConfigured(settings) { + return httpClient, nil + } + t := httpClient.Transport + if isDigestAuthConfigured(settings) { + // if we are using Digest, the Transport is 'digest.Transport' that wraps 'http.Transport' + t = t.(*digest.Transport).Transport + } else if isOAuthCredentialsConfigured(settings) || isOAuthJWTConfigured(settings) { + // if we are using Oauth, the Transport is 'oauth2.Transport' that wraps 'http.Transport' + t = t.(*oauth2.Transport).Base + } + + // secure socks proxy configuration - checks if enabled inside the function + err := proxy.New(settings.ProxyOpts.ProxyOptions).ConfigureSecureSocksHTTPProxy(t.(*http.Transport)) + if err != nil { + logger.Error("error configuring secure socks proxy", "err", err.Error()) + return nil, fmt.Errorf("error configuring secure socks proxy. %s", err) + } + return httpClient, nil +} diff --git a/pkg/httpclient/tls.go b/pkg/httpclient/tls.go new file mode 100644 index 00000000..854fb433 --- /dev/null +++ b/pkg/httpclient/tls.go @@ -0,0 +1,35 @@ +package httpclient + +import ( + "crypto/tls" + "crypto/x509" + "errors" + + "github.com/grafana/grafana-infinity-datasource/pkg/models" +) + +func GetTLSConfigFromSettings(settings models.InfinitySettings) (*tls.Config, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: settings.InsecureSkipVerify, + ServerName: settings.ServerName, + } + if settings.TLSClientAuth { + if settings.TLSClientCert == "" || settings.TLSClientKey == "" { + return nil, errors.New("invalid Client cert or key") + } + cert, err := tls.X509KeyPair([]byte(settings.TLSClientCert), []byte(settings.TLSClientKey)) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + if settings.TLSAuthWithCACert && settings.TLSCACert != "" { + caPool := x509.NewCertPool() + ok := caPool.AppendCertsFromPEM([]byte(settings.TLSCACert)) + if !ok { + return nil, errors.New("invalid TLS CA certificate") + } + tlsConfig.RootCAs = caPool + } + return tlsConfig, nil +} diff --git a/pkg/httpclient/tls_test.go b/pkg/httpclient/tls_test.go new file mode 100644 index 00000000..98349f0f --- /dev/null +++ b/pkg/httpclient/tls_test.go @@ -0,0 +1,164 @@ +package httpclient_test + +import ( + "crypto/tls" + "errors" + "testing" + + "github.com/grafana/grafana-infinity-datasource/pkg/httpclient" + "github.com/grafana/grafana-infinity-datasource/pkg/models" + "github.com/stretchr/testify/assert" +) + +func Test_getTLSConfigFromSettings(t *testing.T) { + t.Run("default settings should return default client", func(t *testing.T) { + got, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{}) + assert.Equal(t, nil, err) + assert.Equal(t, &tls.Config{}, got) + assert.Equal(t, false, got.InsecureSkipVerify) + }) + t.Run("InsecureSkipVerify settings", func(t *testing.T) { + got, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + InsecureSkipVerify: true, + }) + assert.Equal(t, nil, err) + assert.Equal(t, &tls.Config{ + InsecureSkipVerify: true, + }, got) + }) + t.Run("InsecureSkipVerify settings with Servername", func(t *testing.T) { + got, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + InsecureSkipVerify: true, + ServerName: "foo", + }) + assert.Equal(t, nil, err) + assert.Equal(t, &tls.Config{ + InsecureSkipVerify: true, + ServerName: "foo", + }, got) + }) + t.Run("invalid TLSAuthWithCACert should throw error", func(t *testing.T) { + _, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + TLSAuthWithCACert: true, + TLSCACert: "hello", + }) + assert.Equal(t, errors.New("invalid TLS CA certificate"), err) + }) + t.Run("valid TLSAuthWithCACert should not throw error", func(t *testing.T) { + _, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + TLSAuthWithCACert: true, + TLSCACert: mockPEMClientCACet, + }) + assert.Equal(t, nil, err) + }) + t.Run("empty TLSClientCert should throw error", func(t *testing.T) { + _, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + TLSClientAuth: true, + }) + assert.Equal(t, errors.New("invalid Client cert or key"), err) + }) + t.Run("invalid TLSClientCert should throw error", func(t *testing.T) { + _, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + TLSClientAuth: true, + TLSClientCert: "hello", + TLSClientKey: "hello", + }) + assert.Equal(t, errors.New("tls: failed to find any PEM data in certificate input"), err) + }) + t.Run("valid TLSClientCert should not throw error", func(t *testing.T) { + _, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + TLSClientAuth: true, + TLSClientCert: mockClientCert, + TLSClientKey: mockClientKey, + }) + assert.Equal(t, nil, err) + }) + t.Run("valid TLS settings should not throw error", func(t *testing.T) { + got, err := httpclient.GetTLSConfigFromSettings(models.InfinitySettings{ + InsecureSkipVerify: true, + TLSClientAuth: true, + TLSClientCert: mockClientCert, + TLSClientKey: mockClientKey, + TLSAuthWithCACert: true, + TLSCACert: mockPEMClientCACet, + }) + assert.Equal(t, nil, err) + assert.Equal(t, true, got.InsecureSkipVerify) + }) +} + +const ( + mockPEMClientCACet = `-----BEGIN CERTIFICATE----- +MIID3jCCAsagAwIBAgIgfeRMmudbqVL25f2u2vfOW1D94ak+ste/pCrVBCAZemow +DQYJKoZIhvcNAQEFBQAwfzEJMAcGA1UEBhMAMRAwDgYDVQQKDAdleGFtcGxlMRAw +DgYDVQQLDAdleGFtcGxlMRQwEgYDVQQDDAtleGFtcGxlLmNvbTEiMCAGCSqGSIb3 +DQEJARYTaGVsbG9AbG9jYWxob3N0LmNvbTEUMBIGA1UEAwwLZXhhbXBsZS5jb20w +HhcNMjEwNTEyMjExNDE3WhcNMzEwNTEzMjExNDE3WjBpMQkwBwYDVQQGEwAxEDAO +BgNVBAoMB2V4YW1wbGUxEDAOBgNVBAsMB2V4YW1wbGUxFDASBgNVBAMMC2V4YW1w +bGUuY29tMSIwIAYJKoZIhvcNAQkBFhNoZWxsb0Bsb2NhbGhvc3QuY29tMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr2Sc7JXdo94OBImxLauD20fHLAMt +rSFzUMlPJTYalGhuUXRfT6oIr4uf3jydCHT0kkoBKSOurl230Vj8dArN5Pe/+xFM +tgBmSCiFF7NcdvvW8VH5OmJK7j89OAt7DqIzeecqziNBTnWoxnDXbzv4EG994MEU +BtKO8EKPFpxpa5dppN6wDzzLhV1GuhGZRo0aI/Fg4AXWMD3UX2NFHyc7VymhetFL +enereKqQNhMghZL9x/SYkV0j4hkx3dT6t6YthJ0W1E/ATPwyCeNBdTuSVeQe5tm3 +QsLIhLf8h5vBphtGClPAdcmKpujOpraBVNk1KGE3Ij+l/sx2lHt031pzxwIDAQAB +o1wwWjAdBgNVHQ4EFgQUjD6ckZ1Y3SA71L+kgT6JqzNWr3AwHwYDVR0jBBgwFoAU +jD6ckZ1Y3SA71L+kgT6JqzNWr3AwGAYDVR0RBBEwD4INKi5leGFtcGxlLmNvbTAN +BgkqhkiG9w0BAQUFAAOCAQEAQdNZna5iggoJErqNDjysHKAHd+ckLLZrDe4uM7SZ +hk3PdO29Ez5Is0aM4ZdYm2Jl0T5PR79adC4d5wHB4GRDBk0IFZmaTZnYmoRQGa0a +O0dRF0i35jbpWudqeKDi+dyWl05NVDC7TY9uLByqNxUgaG21/BMhxjgR4GI8vbEP +rF3wUqxK2LawghsB7hzT/XWZmAwz56nMKasfV2Mf2UhpnkALIfeEcwuLxVdvUqsV +kxoDsydZaDV+uf8aeQYZvvc9qvONSXWuDcU7uMr9PioXgSHwSOO8UrPbb16TOuhi +WVZwQfmwUtNEQ3zkAYo2g4ZL/LJsmvrmEqwD7csToi/HtQ== +-----END CERTIFICATE-----` + mockClientCert = `-----BEGIN CERTIFICATE----- +MIID4zCCAsugAwIBAgIgH+7x+fQuPf1fUiqXgk7Cp9owHJYKT7RfrrMDnf5Nn6ow +DQYJKoZIhvcNAQEFBQAwgYExCTAHBgNVBAYTADEQMA4GA1UECgwHZXhhbXBsZTEQ +MA4GA1UECwwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20xJDAiBgkqhkiG +9w0BCQEWFWV4YW1wbGVAbG9jYWxob3N0LmNvbTEUMBIGA1UEAwwLZXhhbXBsZS5j +b20wHhcNMjEwNTEyMjExNzE0WhcNMzEwNTEzMjExNzE0WjBrMQkwBwYDVQQGEwAx +EDAOBgNVBAoMB2V4YW1wbGUxEDAOBgNVBAsMB2V4YW1wbGUxFDASBgNVBAMMC2V4 +YW1wbGUuY29tMSQwIgYJKoZIhvcNAQkBFhVleGFtcGxlQGxvY2FsaG9zdC5jb20w +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4p/RMDtjY4GtaX+Wi4Bhi +0TeqxRdfPcn8TivrVNk3D9LtVrO6z7+63GyDMyFNcymc7cUN4gtcyUwUzvYkmMzC +1IRDlmAhw6nFGhhZXyrouWUZNoW1eqiRe6+rQ2UYh3/X4yQ1fyBfj7W+QdjFDSt6 +tpILn2R1HwJk9udt6pG00LGUESAoPu0gAbBjRF2mgT+PtrdFf+ZJbG/lGJIzRhMU +rH7SL+kVQF2l7ZsY5usK0uWl2XoPuVfAsz/es+7C49wE3s63ECU5vwFK1OEbqcBc +jbXRz6h0FJcIPMvtzs9lLokZe2UtvimN4cg3g9dRYhe4UmUBxtpg/UHNrivcCJNH +AgMBAAGjXDBaMB0GA1UdDgQWBBTDM3rROqCZPxpAKgSf9HtXLAfliTAfBgNVHSME +GDAWgBTDM3rROqCZPxpAKgSf9HtXLAfliTAYBgNVHREEETAPgg0qLmV4YW1wbGUu +Y29tMA0GCSqGSIb3DQEBBQUAA4IBAQAHIWPv/LYK3Cx2+9XSRH68hWBJZ7fYHPMz +Jx+EGwcIhGw+iVyiHpHKlv0euZgLUOhSwRakA6XQd3xyAXmccxE7Ckus2mPv31ho +tEO4/LEK3LQLCdJR0iiCbA+MhggB/UCURGOxp0Kc7H2KPFcpn6DbPqz9bKL4RYpq +7uEYT8yoAx+hTsB1ksI16LcOGnRXkU1MvJ4P4NO22tVQo9tLwXPHuYo86Hbh9pq2 +nNdCWucR7xrP8agn/WckpkM63aHBln7hWiMiS/Sk8Y0F+aZDDFU+VtusHwOtYUiP +VgHrQdHpGg7AdnwqcdXBDBhm2gJn2IhpWX2cvuY9lokuXwAPbcdJ +-----END CERTIFICATE-----` + mockClientKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAuKf0TA7Y2OBrWl/louAYYtE3qsUXXz3J/E4r61TZNw/S7Vaz +us+/utxsgzMhTXMpnO3FDeILXMlMFM72JJjMwtSEQ5ZgIcOpxRoYWV8q6LllGTaF +tXqokXuvq0NlGId/1+MkNX8gX4+1vkHYxQ0reraSC59kdR8CZPbnbeqRtNCxlBEg +KD7tIAGwY0RdpoE/j7a3RX/mSWxv5RiSM0YTFKx+0i/pFUBdpe2bGObrCtLlpdl6 +D7lXwLM/3rPuwuPcBN7OtxAlOb8BStThG6nAXI210c+odBSXCDzL7c7PZS6JGXtl +Lb4pjeHIN4PXUWIXuFJlAcbaYP1Bza4r3AiTRwIDAQABAoIBACgpEydVlVD54i9K +Kwn0/ijDwv0nl3E14Y+3urKYhhOFJAVNdZJ8K4Fq/ki8npIXKWZBijl+P6Vi/GKM +LpmACAyZptiCRI8jXHGLPt91JMJvy+6jXoo9TpsxkN/JLRwcIDBmbNIbv4E5Irhp +3sjgl+O9AF95v6H/aAhocKYFvcHawMSTsGU++okI7FyDqQgaam7f+MmazpWM6DOX +cvdzIHvl3FmvApfuBZsGWPpcWcVqXrFWQiOZAvp9cgJLfesklGRSDq3I4ttG0ZYS +pslFShelazzX2ngbUA5GXpJfsGKVXWNV3kOYietJLwZ8uLJMkPDBBwjZB0vdL8Dz +AqEkxuUCgYEA7i/SfJwwR+ZVbojuABvIobguo21t5RawvsM1E714PgTR8uoFSFtr +y41Lc+3uVZqgNv621S2jQknqHrBBLdk8aPonI6UKIrxf3i1PR8akaM01ed1PwMnR +ATE2S1eqruOZb8x1/e6EO29qT6Vs+TP78OhiqvTqUfIcoiPnRELoCu0CgYEAxndE +ACTExNFL0fgUXmPo0mg2zacr0ctDTFQ/R7NO3uUVY78VmZ+kUbT9SNcNvSTnr6xD +kOwyAIfwdo+0UIW/tFSABtDDSK94JAGdr7+LEcQ/QyAmp1KDTmaFrKOkijd/9bev +FVa43+ykdNmKbmHXfvvL6tVMrPwTADLNR3yLbIMCgYEAsCp+q9t5ejRKC68LGNlz +0ui+1fEhzsaxguYuY6NHQ9ec0OV1csbrO2oN3HimRnpO9V3/LDzM+0Jf/sKt8pMx +sxMRz7NJg9d/sHwinxu0ji741mFxk02xYAhd9+unOiLsYVwACQhYlP0azD22E7r3 +JH88OuVaSbGgq+uSKVKy/SECgYEAmHdJQz779yO+tqh5pWXll7a921GA5WPc6IeU +MZX7klq1CvLiOimdR7PeHRYxFMyEPL3/DheV9jh4r+yIHpARjQyZaiL40x8SEb84 +D6r7wINeAkhxyXsnKpSyPsVcg15NrEwXcjI0Rrp6QNZadaAut/viVR7WD9J7Glzs +vO1eAtcCgYEAkStplP9m3IM65eg+OdMqVD4CPohTfmfL/wzailB9QzTy9SNaEvfV +JIoTknAYsX8acOy3XzTdA+mN139mLnG+Tpu1bbjcJLihtPieo5NVWBi/jZedo0Ex +l7aV0Ij7+2S+ynhQUspKZ+fu3Ng+UuMauX9RpkMsfxRyKuj4WrOMVfI= +-----END RSA PRIVATE KEY-----` +) diff --git a/pkg/infinity/client.go b/pkg/infinity/client.go index fd43635f..801643f2 100644 --- a/pkg/infinity/client.go +++ b/pkg/infinity/client.go @@ -3,8 +3,6 @@ package infinity import ( "bytes" "context" - "crypto/tls" - "crypto/x509" "encoding/json" "errors" "fmt" @@ -16,13 +14,11 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" + "github.com/grafana/grafana-infinity-datasource/pkg/httpclient" "github.com/grafana/grafana-infinity-datasource/pkg/models" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" - "github.com/icholy/digest" - "golang.org/x/oauth2" ) type Client struct { @@ -32,60 +28,6 @@ type Client struct { IsMock bool } -func GetTLSConfigFromSettings(settings models.InfinitySettings) (*tls.Config, error) { - tlsConfig := &tls.Config{ - InsecureSkipVerify: settings.InsecureSkipVerify, - ServerName: settings.ServerName, - } - if settings.TLSClientAuth { - if settings.TLSClientCert == "" || settings.TLSClientKey == "" { - return nil, errors.New("invalid Client cert or key") - } - cert, err := tls.X509KeyPair([]byte(settings.TLSClientCert), []byte(settings.TLSClientKey)) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if settings.TLSAuthWithCACert && settings.TLSCACert != "" { - caPool := x509.NewCertPool() - ok := caPool.AppendCertsFromPEM([]byte(settings.TLSCACert)) - if !ok { - return nil, errors.New("invalid TLS CA certificate") - } - tlsConfig.RootCAs = caPool - } - return tlsConfig, nil -} - -func getBaseHTTPClient(ctx context.Context, settings models.InfinitySettings) *http.Client { - logger := backend.Logger.FromContext(ctx) - tlsConfig, err := GetTLSConfigFromSettings(settings) - if err != nil { - return nil - } - transport := &http.Transport{TLSClientConfig: tlsConfig} - switch settings.ProxyType { - case models.ProxyTypeNone: - logger.Debug("proxy type is set to none. Not using the proxy") - case models.ProxyTypeUrl: - logger.Debug("proxy type is set to url. Using the proxy", "proxy_url", settings.ProxyUrl) - u, err := url.Parse(settings.ProxyUrl) - if err != nil { - logger.Error("error parsing proxy url", "err", err.Error(), "proxy_url", settings.ProxyUrl) - return nil - } - transport.Proxy = http.ProxyURL(u) - default: - transport.Proxy = http.ProxyFromEnvironment - } - - return &http.Client{ - Transport: transport, - Timeout: time.Second * time.Duration(settings.TimeoutInSeconds), - } -} - func NewClient(ctx context.Context, settings models.InfinitySettings) (client *Client, err error) { logger := backend.Logger.FromContext(ctx) _, span := tracing.DefaultTracer().Start(ctx, "NewClient") @@ -99,28 +41,16 @@ func NewClient(ctx context.Context, settings models.InfinitySettings) (client *C settings.AuthenticationMethod = models.AuthenticationMethodForwardOauth } } - httpClient := getBaseHTTPClient(ctx, settings) - if httpClient == nil { + httpClient, err := httpclient.GetHTTPClient(ctx, settings) + if err != nil { span.RecordError(errors.New("invalid http client")) logger.Error("invalid http client", "datasource uid", settings.UID, "datasource name", settings.Name) - return client, errors.New("invalid http client") - } - httpClient = ApplyDigestAuth(ctx, httpClient, settings) - httpClient = ApplyOAuthClientCredentials(ctx, httpClient, settings) - httpClient = ApplyOAuthJWT(ctx, httpClient, settings) - httpClient = ApplyAWSAuth(ctx, httpClient, settings) - - httpClient, err = ApplySecureSocksProxyConfiguration(ctx, httpClient, settings) - if err != nil { - logger.Error("error applying secure socks proxy", "datasource uid", settings.UID, "datasource name", settings.Name) return client, err } - client = &Client{ Settings: settings, HttpClient: httpClient, } - if settings.AuthenticationMethod == models.AuthenticationMethodAzureBlob { cred, err := azblob.NewSharedKeyCredential(settings.AzureBlobAccountName, settings.AzureBlobAccountKey) if err != nil { @@ -157,29 +87,6 @@ func NewClient(ctx context.Context, settings models.InfinitySettings) (client *C return client, err } -func ApplySecureSocksProxyConfiguration(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { - logger := backend.Logger.FromContext(ctx) - if IsAwsAuthConfigured(settings) { - return httpClient, nil - } - t := httpClient.Transport - if IsDigestAuthConfigured(settings) { - // if we are using Digest, the Transport is 'digest.Transport' that wraps 'http.Transport' - t = t.(*digest.Transport).Transport - } else if IsOAuthCredentialsConfigured(settings) || IsOAuthJWTConfigured(settings) { - // if we are using Oauth, the Transport is 'oauth2.Transport' that wraps 'http.Transport' - t = t.(*oauth2.Transport).Base - } - - // secure socks proxy configuration - checks if enabled inside the function - err := proxy.New(settings.ProxyOpts.ProxyOptions).ConfigureSecureSocksHTTPProxy(t.(*http.Transport)) - if err != nil { - logger.Error("error configuring secure socks proxy", "err", err.Error()) - return nil, fmt.Errorf("error configuring secure socks proxy. %s", err) - } - return httpClient, nil -} - func replaceSect(input string, settings models.InfinitySettings, includeSect bool) string { for key, value := range settings.SecureQueryFields { if includeSect { @@ -234,7 +141,7 @@ func (client *Client) req(ctx context.Context, url string, body io.Reader, setti return nil, http.StatusInternalServerError, duration, errorsource.DownstreamError(fmt.Errorf("invalid response received for the URL %s", url), false) } if res.StatusCode >= http.StatusBadRequest { - err = fmt.Errorf("%w. %s", ErrUnsuccessfulHTTPResponseStatus, res.Status) + err = fmt.Errorf("%w. %s", models.ErrUnsuccessfulHTTPResponseStatus, res.Status) // Infinity can query anything and users are responsible for ensuring that endpoint/auth is correct // therefore any incoming error is considered downstream return nil, res.StatusCode, duration, errorsource.DownstreamError(err, false) @@ -249,7 +156,7 @@ func (client *Client) req(ctx context.Context, url string, body io.Reader, setti var out any err := json.Unmarshal(bodyBytes, &out) if err != nil { - err = fmt.Errorf("%w. %w", ErrParsingResponseBodyAsJson, err) + err = fmt.Errorf("%w. %w", models.ErrParsingResponseBodyAsJson, err) err = errorsource.DownstreamError(err, false) logger.Debug("error un-marshaling JSON response", "url", url, "error", err.Error()) } diff --git a/pkg/infinity/client_test.go b/pkg/infinity/client_test.go index 1ad8bfcd..558ada9b 100644 --- a/pkg/infinity/client_test.go +++ b/pkg/infinity/client_test.go @@ -2,8 +2,6 @@ package infinity_test import ( "context" - "crypto/tls" - "errors" "fmt" "net/http" "testing" @@ -120,83 +118,6 @@ func TestCanAllowURL(t *testing.T) { } } -func Test_getTLSConfigFromSettings(t *testing.T) { - t.Run("default settings should return default client", func(t *testing.T) { - got, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{}) - assert.Equal(t, nil, err) - assert.Equal(t, &tls.Config{}, got) - assert.Equal(t, false, got.InsecureSkipVerify) - }) - t.Run("InsecureSkipVerify settings", func(t *testing.T) { - got, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - InsecureSkipVerify: true, - }) - assert.Equal(t, nil, err) - assert.Equal(t, &tls.Config{ - InsecureSkipVerify: true, - }, got) - }) - t.Run("InsecureSkipVerify settings with Servername", func(t *testing.T) { - got, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - InsecureSkipVerify: true, - ServerName: "foo", - }) - assert.Equal(t, nil, err) - assert.Equal(t, &tls.Config{ - InsecureSkipVerify: true, - ServerName: "foo", - }, got) - }) - t.Run("invalid TLSAuthWithCACert should throw error", func(t *testing.T) { - _, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - TLSAuthWithCACert: true, - TLSCACert: "hello", - }) - assert.Equal(t, errors.New("invalid TLS CA certificate"), err) - }) - t.Run("valid TLSAuthWithCACert should not throw error", func(t *testing.T) { - _, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - TLSAuthWithCACert: true, - TLSCACert: mockPEMClientCACet, - }) - assert.Equal(t, nil, err) - }) - t.Run("empty TLSClientCert should throw error", func(t *testing.T) { - _, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - TLSClientAuth: true, - }) - assert.Equal(t, errors.New("invalid Client cert or key"), err) - }) - t.Run("invalid TLSClientCert should throw error", func(t *testing.T) { - _, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - TLSClientAuth: true, - TLSClientCert: "hello", - TLSClientKey: "hello", - }) - assert.Equal(t, errors.New("tls: failed to find any PEM data in certificate input"), err) - }) - t.Run("valid TLSClientCert should not throw error", func(t *testing.T) { - _, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - TLSClientAuth: true, - TLSClientCert: mockClientCert, - TLSClientKey: mockClientKey, - }) - assert.Equal(t, nil, err) - }) - t.Run("valid TLS settings should not throw error", func(t *testing.T) { - got, err := infinity.GetTLSConfigFromSettings(models.InfinitySettings{ - InsecureSkipVerify: true, - TLSClientAuth: true, - TLSClientCert: mockClientCert, - TLSClientKey: mockClientKey, - TLSAuthWithCACert: true, - TLSCACert: mockPEMClientCACet, - }) - assert.Equal(t, nil, err) - assert.Equal(t, true, got.InsecureSkipVerify) - }) -} - const ( mockCSVDomain = "https://gist.githubusercontent.com" mockCSVURL = "/yesoreyeram/64a46b02f0bf87cb527d6270dd84ea47/raw/32ae9b1a4a0183dceb3596226b818c8f428193af/sample-with-quotes.csv" @@ -208,79 +129,6 @@ const ( mockXMLURL = "/yesoreyeram/655a362eed0f51be24e16d3f1127a31d/raw/0cdc6302b7c6a2dec69606d9471b56c843863054/simple.xml" mockXMLDATA = ` Empire BurlesqueHide your heart` - mockJSONDomain = "https://gist.githubusercontent.com" - mockJSONURL = "/yesoreyeram/655a362eed0f51be24e16d3f1127a31d/raw/7b5dac1fe0a5d5ce47c9251117f73ade363b7ca8/users.json" - mockPEMClientCACet = `-----BEGIN CERTIFICATE----- -MIID3jCCAsagAwIBAgIgfeRMmudbqVL25f2u2vfOW1D94ak+ste/pCrVBCAZemow -DQYJKoZIhvcNAQEFBQAwfzEJMAcGA1UEBhMAMRAwDgYDVQQKDAdleGFtcGxlMRAw -DgYDVQQLDAdleGFtcGxlMRQwEgYDVQQDDAtleGFtcGxlLmNvbTEiMCAGCSqGSIb3 -DQEJARYTaGVsbG9AbG9jYWxob3N0LmNvbTEUMBIGA1UEAwwLZXhhbXBsZS5jb20w -HhcNMjEwNTEyMjExNDE3WhcNMzEwNTEzMjExNDE3WjBpMQkwBwYDVQQGEwAxEDAO -BgNVBAoMB2V4YW1wbGUxEDAOBgNVBAsMB2V4YW1wbGUxFDASBgNVBAMMC2V4YW1w -bGUuY29tMSIwIAYJKoZIhvcNAQkBFhNoZWxsb0Bsb2NhbGhvc3QuY29tMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr2Sc7JXdo94OBImxLauD20fHLAMt -rSFzUMlPJTYalGhuUXRfT6oIr4uf3jydCHT0kkoBKSOurl230Vj8dArN5Pe/+xFM -tgBmSCiFF7NcdvvW8VH5OmJK7j89OAt7DqIzeecqziNBTnWoxnDXbzv4EG994MEU -BtKO8EKPFpxpa5dppN6wDzzLhV1GuhGZRo0aI/Fg4AXWMD3UX2NFHyc7VymhetFL -enereKqQNhMghZL9x/SYkV0j4hkx3dT6t6YthJ0W1E/ATPwyCeNBdTuSVeQe5tm3 -QsLIhLf8h5vBphtGClPAdcmKpujOpraBVNk1KGE3Ij+l/sx2lHt031pzxwIDAQAB -o1wwWjAdBgNVHQ4EFgQUjD6ckZ1Y3SA71L+kgT6JqzNWr3AwHwYDVR0jBBgwFoAU -jD6ckZ1Y3SA71L+kgT6JqzNWr3AwGAYDVR0RBBEwD4INKi5leGFtcGxlLmNvbTAN -BgkqhkiG9w0BAQUFAAOCAQEAQdNZna5iggoJErqNDjysHKAHd+ckLLZrDe4uM7SZ -hk3PdO29Ez5Is0aM4ZdYm2Jl0T5PR79adC4d5wHB4GRDBk0IFZmaTZnYmoRQGa0a -O0dRF0i35jbpWudqeKDi+dyWl05NVDC7TY9uLByqNxUgaG21/BMhxjgR4GI8vbEP -rF3wUqxK2LawghsB7hzT/XWZmAwz56nMKasfV2Mf2UhpnkALIfeEcwuLxVdvUqsV -kxoDsydZaDV+uf8aeQYZvvc9qvONSXWuDcU7uMr9PioXgSHwSOO8UrPbb16TOuhi -WVZwQfmwUtNEQ3zkAYo2g4ZL/LJsmvrmEqwD7csToi/HtQ== ------END CERTIFICATE-----` - mockClientCert = `-----BEGIN CERTIFICATE----- -MIID4zCCAsugAwIBAgIgH+7x+fQuPf1fUiqXgk7Cp9owHJYKT7RfrrMDnf5Nn6ow -DQYJKoZIhvcNAQEFBQAwgYExCTAHBgNVBAYTADEQMA4GA1UECgwHZXhhbXBsZTEQ -MA4GA1UECwwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20xJDAiBgkqhkiG -9w0BCQEWFWV4YW1wbGVAbG9jYWxob3N0LmNvbTEUMBIGA1UEAwwLZXhhbXBsZS5j -b20wHhcNMjEwNTEyMjExNzE0WhcNMzEwNTEzMjExNzE0WjBrMQkwBwYDVQQGEwAx -EDAOBgNVBAoMB2V4YW1wbGUxEDAOBgNVBAsMB2V4YW1wbGUxFDASBgNVBAMMC2V4 -YW1wbGUuY29tMSQwIgYJKoZIhvcNAQkBFhVleGFtcGxlQGxvY2FsaG9zdC5jb20w -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4p/RMDtjY4GtaX+Wi4Bhi -0TeqxRdfPcn8TivrVNk3D9LtVrO6z7+63GyDMyFNcymc7cUN4gtcyUwUzvYkmMzC -1IRDlmAhw6nFGhhZXyrouWUZNoW1eqiRe6+rQ2UYh3/X4yQ1fyBfj7W+QdjFDSt6 -tpILn2R1HwJk9udt6pG00LGUESAoPu0gAbBjRF2mgT+PtrdFf+ZJbG/lGJIzRhMU -rH7SL+kVQF2l7ZsY5usK0uWl2XoPuVfAsz/es+7C49wE3s63ECU5vwFK1OEbqcBc -jbXRz6h0FJcIPMvtzs9lLokZe2UtvimN4cg3g9dRYhe4UmUBxtpg/UHNrivcCJNH -AgMBAAGjXDBaMB0GA1UdDgQWBBTDM3rROqCZPxpAKgSf9HtXLAfliTAfBgNVHSME -GDAWgBTDM3rROqCZPxpAKgSf9HtXLAfliTAYBgNVHREEETAPgg0qLmV4YW1wbGUu -Y29tMA0GCSqGSIb3DQEBBQUAA4IBAQAHIWPv/LYK3Cx2+9XSRH68hWBJZ7fYHPMz -Jx+EGwcIhGw+iVyiHpHKlv0euZgLUOhSwRakA6XQd3xyAXmccxE7Ckus2mPv31ho -tEO4/LEK3LQLCdJR0iiCbA+MhggB/UCURGOxp0Kc7H2KPFcpn6DbPqz9bKL4RYpq -7uEYT8yoAx+hTsB1ksI16LcOGnRXkU1MvJ4P4NO22tVQo9tLwXPHuYo86Hbh9pq2 -nNdCWucR7xrP8agn/WckpkM63aHBln7hWiMiS/Sk8Y0F+aZDDFU+VtusHwOtYUiP -VgHrQdHpGg7AdnwqcdXBDBhm2gJn2IhpWX2cvuY9lokuXwAPbcdJ ------END CERTIFICATE-----` - mockClientKey = `-----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAuKf0TA7Y2OBrWl/louAYYtE3qsUXXz3J/E4r61TZNw/S7Vaz -us+/utxsgzMhTXMpnO3FDeILXMlMFM72JJjMwtSEQ5ZgIcOpxRoYWV8q6LllGTaF -tXqokXuvq0NlGId/1+MkNX8gX4+1vkHYxQ0reraSC59kdR8CZPbnbeqRtNCxlBEg -KD7tIAGwY0RdpoE/j7a3RX/mSWxv5RiSM0YTFKx+0i/pFUBdpe2bGObrCtLlpdl6 -D7lXwLM/3rPuwuPcBN7OtxAlOb8BStThG6nAXI210c+odBSXCDzL7c7PZS6JGXtl -Lb4pjeHIN4PXUWIXuFJlAcbaYP1Bza4r3AiTRwIDAQABAoIBACgpEydVlVD54i9K -Kwn0/ijDwv0nl3E14Y+3urKYhhOFJAVNdZJ8K4Fq/ki8npIXKWZBijl+P6Vi/GKM -LpmACAyZptiCRI8jXHGLPt91JMJvy+6jXoo9TpsxkN/JLRwcIDBmbNIbv4E5Irhp -3sjgl+O9AF95v6H/aAhocKYFvcHawMSTsGU++okI7FyDqQgaam7f+MmazpWM6DOX -cvdzIHvl3FmvApfuBZsGWPpcWcVqXrFWQiOZAvp9cgJLfesklGRSDq3I4ttG0ZYS -pslFShelazzX2ngbUA5GXpJfsGKVXWNV3kOYietJLwZ8uLJMkPDBBwjZB0vdL8Dz -AqEkxuUCgYEA7i/SfJwwR+ZVbojuABvIobguo21t5RawvsM1E714PgTR8uoFSFtr -y41Lc+3uVZqgNv621S2jQknqHrBBLdk8aPonI6UKIrxf3i1PR8akaM01ed1PwMnR -ATE2S1eqruOZb8x1/e6EO29qT6Vs+TP78OhiqvTqUfIcoiPnRELoCu0CgYEAxndE -ACTExNFL0fgUXmPo0mg2zacr0ctDTFQ/R7NO3uUVY78VmZ+kUbT9SNcNvSTnr6xD -kOwyAIfwdo+0UIW/tFSABtDDSK94JAGdr7+LEcQ/QyAmp1KDTmaFrKOkijd/9bev -FVa43+ykdNmKbmHXfvvL6tVMrPwTADLNR3yLbIMCgYEAsCp+q9t5ejRKC68LGNlz -0ui+1fEhzsaxguYuY6NHQ9ec0OV1csbrO2oN3HimRnpO9V3/LDzM+0Jf/sKt8pMx -sxMRz7NJg9d/sHwinxu0ji741mFxk02xYAhd9+unOiLsYVwACQhYlP0azD22E7r3 -JH88OuVaSbGgq+uSKVKy/SECgYEAmHdJQz779yO+tqh5pWXll7a921GA5WPc6IeU -MZX7klq1CvLiOimdR7PeHRYxFMyEPL3/DheV9jh4r+yIHpARjQyZaiL40x8SEb84 -D6r7wINeAkhxyXsnKpSyPsVcg15NrEwXcjI0Rrp6QNZadaAut/viVR7WD9J7Glzs -vO1eAtcCgYEAkStplP9m3IM65eg+OdMqVD4CPohTfmfL/wzailB9QzTy9SNaEvfV -JIoTknAYsX8acOy3XzTdA+mN139mLnG+Tpu1bbjcJLihtPieo5NVWBi/jZedo0Ex -l7aV0Ij7+2S+ynhQUspKZ+fu3Ng+UuMauX9RpkMsfxRyKuj4WrOMVfI= ------END RSA PRIVATE KEY-----` + mockJSONDomain = "https://gist.githubusercontent.com" + mockJSONURL = "/yesoreyeram/655a362eed0f51be24e16d3f1127a31d/raw/7b5dac1fe0a5d5ce47c9251117f73ade363b7ca8/users.json" ) diff --git a/pkg/infinity/headers.go b/pkg/infinity/headers.go index dade63ca..2d439004 100644 --- a/pkg/infinity/headers.go +++ b/pkg/infinity/headers.go @@ -28,7 +28,7 @@ const ( HeaderKeyIdToken = "X-Id-Token" ) -func ApplyAcceptHeader(query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyAcceptHeader(_ context.Context, query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { if query.Type == models.QueryTypeJSON || query.Type == models.QueryTypeGraphQL { req.Header.Set(headerKeyAccept, `application/json;q=0.9,text/plain`) } @@ -41,7 +41,7 @@ func ApplyAcceptHeader(query models.Query, settings models.InfinitySettings, req return req } -func ApplyContentTypeHeader(query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyContentTypeHeader(_ context.Context, query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { if strings.ToUpper(query.URLOptions.Method) == http.MethodPost { switch query.URLOptions.BodyType { case "raw": @@ -68,7 +68,7 @@ func ApplyContentTypeHeader(query models.Query, settings models.InfinitySettings return req } -func ApplyHeadersFromSettings(settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyHeadersFromSettings(_ context.Context, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { for key, value := range settings.CustomHeaders { val := dummyHeader if includeSect { @@ -84,7 +84,7 @@ func ApplyHeadersFromSettings(settings models.InfinitySettings, req *http.Reques return req } -func ApplyHeadersFromQuery(query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyHeadersFromQuery(_ context.Context, query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { for _, header := range query.URLOptions.Headers { value := dummyHeader if includeSect { @@ -100,7 +100,7 @@ func ApplyHeadersFromQuery(query models.Query, settings models.InfinitySettings, return req } -func ApplyBasicAuth(settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyBasicAuth(_ context.Context, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { if settings.BasicAuthEnabled && (settings.UserName != "" || settings.Password != "") { basicAuthHeader := fmt.Sprintf("Basic %s", dummyHeader) if includeSect { @@ -111,7 +111,7 @@ func ApplyBasicAuth(settings models.InfinitySettings, req *http.Request, include return req } -func ApplyBearerToken(settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyBearerToken(_ context.Context, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { if settings.AuthenticationMethod == models.AuthenticationMethodBearerToken { bearerAuthHeader := fmt.Sprintf("Bearer %s", dummyHeader) if includeSect { @@ -122,7 +122,7 @@ func ApplyBearerToken(settings models.InfinitySettings, req *http.Request, inclu return req } -func ApplyApiKeyAuth(settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyApiKeyAuth(_ context.Context, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { if settings.AuthenticationMethod == models.AuthenticationMethodApiKey && settings.ApiKeyType == models.ApiKeyTypeHeader { apiKeyHeader := dummyHeader if includeSect { @@ -135,7 +135,7 @@ func ApplyApiKeyAuth(settings models.InfinitySettings, req *http.Request, includ return req } -func ApplyForwardedOAuthIdentity(requestHeaders map[string]string, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { +func ApplyForwardedOAuthIdentity(_ context.Context, requestHeaders map[string]string, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request { if settings.ForwardOauthIdentity { authHeader := dummyHeader token := dummyHeader @@ -170,4 +170,3 @@ func getQueryReqHeader(requestHeaders map[string]string, headerName string) stri return "" } - diff --git a/pkg/infinity/oauth.go b/pkg/infinity/oauth.go deleted file mode 100644 index 27d51b0b..00000000 --- a/pkg/infinity/oauth.go +++ /dev/null @@ -1,128 +0,0 @@ -package infinity - -import ( - "context" - "net/http" - "net/url" - "strings" - - "github.com/grafana/grafana-aws-sdk/pkg/awsds" - "github.com/grafana/grafana-aws-sdk/pkg/sigv4" - "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" - "github.com/icholy/digest" - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" - "golang.org/x/oauth2/jwt" - - "github.com/grafana/grafana-infinity-datasource/pkg/models" -) - -func ApplyOAuthClientCredentials(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) *http.Client { - _, span := tracing.DefaultTracer().Start(ctx, "ApplyOAuthClientCredentials") - defer span.End() - if IsOAuthCredentialsConfigured(settings) { - oauthConfig := clientcredentials.Config{ - ClientID: settings.OAuth2Settings.ClientID, - ClientSecret: settings.OAuth2Settings.ClientSecret, - TokenURL: settings.OAuth2Settings.TokenURL, - Scopes: []string{}, - EndpointParams: url.Values{}, - AuthStyle: settings.OAuth2Settings.AuthStyle, - } - for _, scope := range settings.OAuth2Settings.Scopes { - if scope != "" { - oauthConfig.Scopes = append(oauthConfig.Scopes, scope) - } - } - for k, v := range settings.OAuth2Settings.EndpointParams { - if k != "" && v != "" { - oauthConfig.EndpointParams.Set(k, v) - } - } - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) - httpClient = oauthConfig.Client(ctx) - } - return httpClient -} - -func IsOAuthCredentialsConfigured(settings models.InfinitySettings) bool { - return settings.AuthenticationMethod == models.AuthenticationMethodOAuth && settings.OAuth2Settings.OAuth2Type == models.AuthOAuthTypeClientCredentials -} - -func ApplyOAuthJWT(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) *http.Client { - _, span := tracing.DefaultTracer().Start(ctx, "ApplyOAuthJWT") - defer span.End() - if IsOAuthJWTConfigured(settings) { - jwtConfig := jwt.Config{ - Email: settings.OAuth2Settings.Email, - TokenURL: settings.OAuth2Settings.TokenURL, - PrivateKey: []byte(strings.ReplaceAll(settings.OAuth2Settings.PrivateKey, "\\n", "\n")), - PrivateKeyID: settings.OAuth2Settings.PrivateKeyID, - Subject: settings.OAuth2Settings.Subject, - Scopes: []string{}, - } - for _, scope := range settings.OAuth2Settings.Scopes { - if scope != "" { - jwtConfig.Scopes = append(jwtConfig.Scopes, scope) - } - } - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) - httpClient = jwtConfig.Client(ctx) - } - return httpClient -} - -func IsOAuthJWTConfigured(settings models.InfinitySettings) bool { - return settings.AuthenticationMethod == models.AuthenticationMethodOAuth && settings.OAuth2Settings.OAuth2Type == models.AuthOAuthJWT -} -func ApplyDigestAuth(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) *http.Client { - _, span := tracing.DefaultTracer().Start(ctx, "ApplyDigestAuth") - defer span.End() - if IsDigestAuthConfigured(settings) { - a := digest.Transport{Username: settings.UserName, Password: settings.Password, Transport: httpClient.Transport} - httpClient.Transport = &a - } - return httpClient -} - -func IsDigestAuthConfigured(settings models.InfinitySettings) bool { - return settings.AuthenticationMethod == models.AuthenticationMethodDigestAuth -} -func ApplyAWSAuth(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) *http.Client { - ctx, span := tracing.DefaultTracer().Start(ctx, "ApplyAWSAuth") - defer span.End() - if IsAwsAuthConfigured(settings) { - tempHttpClient := getBaseHTTPClient(ctx, settings) - authType := settings.AWSSettings.AuthType - if authType == "" { - authType = models.AWSAuthTypeKeys - } - region := settings.AWSSettings.Region - if region == "" { - region = "us-east-2" - } - service := settings.AWSSettings.Service - if service == "" { - service = "monitoring" - } - conf := &sigv4.Config{ - AuthType: string(authType), - Region: region, - Service: service, - AccessKey: settings.AWSAccessKey, - SecretKey: settings.AWSSecretKey, - } - - authSettings := awsds.ReadAuthSettings(ctx) - rt, _ := sigv4.New(conf, *authSettings, sigv4.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { - req.Header.Add("Accept", "application/json") - return tempHttpClient.Do(req) - })) - httpClient.Transport = rt - } - return httpClient -} - -func IsAwsAuthConfigured(settings models.InfinitySettings) bool { - return settings.AuthenticationMethod == models.AuthenticationMethodAWS -} diff --git a/pkg/infinity/request.go b/pkg/infinity/request.go index 43b72353..17c750e1 100644 --- a/pkg/infinity/request.go +++ b/pkg/infinity/request.go @@ -26,14 +26,14 @@ func GetRequest(ctx context.Context, settings models.InfinitySettings, body io.R default: req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil) } - req = ApplyAcceptHeader(query, settings, req, includeSect) - req = ApplyContentTypeHeader(query, settings, req, includeSect) - req = ApplyHeadersFromSettings(settings, req, includeSect) - req = ApplyHeadersFromQuery(query, settings, req, includeSect) - req = ApplyBasicAuth(settings, req, includeSect) - req = ApplyBearerToken(settings, req, includeSect) - req = ApplyApiKeyAuth(settings, req, includeSect) - req = ApplyForwardedOAuthIdentity(requestHeaders, settings, req, includeSect) + req = ApplyAcceptHeader(ctx, query, settings, req, includeSect) + req = ApplyContentTypeHeader(ctx, query, settings, req, includeSect) + req = ApplyHeadersFromSettings(ctx, settings, req, includeSect) + req = ApplyHeadersFromQuery(ctx, query, settings, req, includeSect) + req = ApplyBasicAuth(ctx, settings, req, includeSect) + req = ApplyBearerToken(ctx, settings, req, includeSect) + req = ApplyApiKeyAuth(ctx, settings, req, includeSect) + req = ApplyForwardedOAuthIdentity(ctx, requestHeaders, settings, req, includeSect) req = ApplyTraceHead(ctx, req) return req, err } diff --git a/pkg/infinity/errors.go b/pkg/models/errors.go similarity index 67% rename from pkg/infinity/errors.go rename to pkg/models/errors.go index 325bd234..ef827efa 100644 --- a/pkg/infinity/errors.go +++ b/pkg/models/errors.go @@ -1,8 +1,9 @@ -package infinity +package models import "errors" var ( ErrUnsuccessfulHTTPResponseStatus error = errors.New("unsuccessful HTTP response") ErrParsingResponseBodyAsJson error = errors.New("unable to parse response body as JSON") + ErrCreatingHTTPClient error = errors.New("error creating HTTP client") ) diff --git a/pkg/testsuite/handler_querydata_errors_test.go b/pkg/testsuite/handler_querydata_errors_test.go index 32f39a1c..4ea65ca9 100644 --- a/pkg/testsuite/handler_querydata_errors_test.go +++ b/pkg/testsuite/handler_querydata_errors_test.go @@ -42,7 +42,7 @@ func TestErrors(t *testing.T) { }, *client, map[string]string{}, backend.PluginContext{}) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) - require.ErrorIs(t, res.Error, infinity.ErrUnsuccessfulHTTPResponseStatus) + require.ErrorIs(t, res.Error, models.ErrUnsuccessfulHTTPResponseStatus) require.Equal(t, "error while performing the infinity query. unsuccessful HTTP response. 403 Forbidden", res.Error.Error()) }) t.Run("fail with incorrect response from server", func(t *testing.T) { @@ -58,7 +58,7 @@ func TestErrors(t *testing.T) { }, *client, map[string]string{}, backend.PluginContext{}) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) - require.ErrorIs(t, res.Error, infinity.ErrParsingResponseBodyAsJson) + require.ErrorIs(t, res.Error, models.ErrParsingResponseBodyAsJson) require.Equal(t, "error while performing the infinity query. unable to parse response body as JSON. unexpected end of JSON input", res.Error.Error()) }) t.Run("fail with incorrect JSONata", func(t *testing.T) {