Просмотр исходного кода

Merge branch 'verify_datasource_tls' of https://github.com/mattbostock/grafana into mattbostock-verify_datasource_tls

bergquist 8 лет назад
Родитель
Сommit
c0d257a0ee

+ 1 - 2
pkg/api/app_routes.go

@@ -6,14 +6,13 @@ import (
 	"net/http"
 	"time"
 
-	"gopkg.in/macaron.v1"
-
 	"github.com/grafana/grafana/pkg/api/pluginproxy"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/util"
+	macaron "gopkg.in/macaron.v1"
 )
 
 var pluginProxyTransport = &http.Transport{

+ 19 - 17
pkg/models/datasource_cache.go

@@ -3,6 +3,7 @@ package models
 import (
 	"crypto/tls"
 	"crypto/x509"
+	"errors"
 	"net"
 	"net/http"
 	"sync"
@@ -45,9 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
 		return t.Transport, nil
 	}
 
+	var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
+	if ds.JsonData != nil {
+		tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
+		tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
+		tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false)
+	}
+
 	transport := &http.Transport{
 		TLSClientConfig: &tls.Config{
-			InsecureSkipVerify: true,
+			InsecureSkipVerify: tlsSkipVerify,
 			Renegotiation:      tls.RenegotiateFreelyAsClient,
 		},
 		Proxy: http.ProxyFromEnvironment,
@@ -62,30 +70,24 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
 		IdleConnTimeout:       90 * time.Second,
 	}
 
-	var tlsAuth, tlsAuthWithCACert bool
-	if ds.JsonData != nil {
-		tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
-		tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
-	}
-
-	if tlsAuth {
-		transport.TLSClientConfig.InsecureSkipVerify = false
-
+	if tlsClientAuth || tlsAuthWithCACert {
 		decrypted := ds.SecureJsonData.Decrypt()
-
 		if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
 			caPool := x509.NewCertPool()
 			ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
-			if ok {
-				transport.TLSClientConfig.RootCAs = caPool
+			if !ok {
+				return nil, errors.New("Failed to parse TLS CA PEM certificate")
 			}
+			transport.TLSClientConfig.RootCAs = caPool
 		}
 
-		cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
-		if err != nil {
-			return nil, err
+		if tlsClientAuth {
+			cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
+			if err != nil {
+				return nil, err
+			}
+			transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
 		}
-		transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
 	}
 
 	ptc.cache[ds.Id] = cachedTransport{

+ 110 - 30
pkg/models/datasource_cache_test.go

@@ -29,61 +29,140 @@ func TestDataSourceCache(t *testing.T) {
 		Convey("Should be using the cached proxy", func() {
 			So(t2, ShouldEqual, t1)
 		})
+		Convey("Should verify TLS by default", func() {
+			So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
+		})
+		Convey("Should have no TLS client certificate configured", func() {
+			So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
+		})
+		Convey("Should have no user-supplied TLS CA onfigured", func() {
+			So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
+		})
 	})
 
-	Convey("When getting kubernetes datasource proxy", t, func() {
+	Convey("When caching a datasource proxy then updating it", t, func() {
 		clearCache()
 		setting.SecretKey = "password"
 
 		json := simplejson.New()
-		json.Set("tlsAuth", true)
 		json.Set("tlsAuthWithCACert", true)
 
-		t := time.Now()
+		tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
+		So(err, ShouldBeNil)
+		ds := DataSource{
+			Id:             1,
+			Url:            "http://k8s:8001",
+			Type:           "Kubernetes",
+			SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
+			Updated:        time.Now().Add(-2 * time.Minute),
+		}
+
+		t1, err := ds.GetHttpTransport()
+		So(err, ShouldBeNil)
+
+		Convey("Should verify TLS by default", func() {
+			So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
+		})
+		Convey("Should have no TLS client certificate configured", func() {
+			So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
+		})
+		Convey("Should have no user-supplied TLS CA configured", func() {
+			So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
+		})
+
+		ds.JsonData = nil
+		ds.SecureJsonData = map[string][]byte{}
+		ds.Updated = time.Now()
+
+		t2, err := ds.GetHttpTransport()
+		So(err, ShouldBeNil)
+
+		Convey("Should have no user-supplied TLS CA configured after the update", func() {
+			So(t2.TLSClientConfig.RootCAs, ShouldBeNil)
+		})
+	})
+
+	Convey("When caching a datasource proxy with TLS client authentication enabled", t, func() {
+		clearCache()
+		setting.SecretKey = "password"
+
+		json := simplejson.New()
+		json.Set("tlsAuth", true)
+
+		tlsClientCert, err := util.Encrypt([]byte(clientCert), "password")
+		So(err, ShouldBeNil)
+		tlsClientKey, err := util.Encrypt([]byte(clientKey), "password")
+		So(err, ShouldBeNil)
+
 		ds := DataSource{
-			Url:     "http://k8s:8001",
-			Type:    "Kubernetes",
-			Updated: t.Add(-2 * time.Minute),
+			Id:       1,
+			Url:      "http://k8s:8001",
+			Type:     "Kubernetes",
+			JsonData: json,
+			SecureJsonData: map[string][]byte{
+				"tlsClientCert": tlsClientCert,
+				"tlsClientKey":  tlsClientKey,
+			},
 		}
 
-		transport, err := ds.GetHttpTransport()
+		tr, err := ds.GetHttpTransport()
 		So(err, ShouldBeNil)
 
-		Convey("Should have no cert", func() {
-			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
+		Convey("Should verify TLS by default", func() {
+			So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
 		})
+		Convey("Should have a TLS client certificate configured", func() {
+			So(len(tr.TLSClientConfig.Certificates), ShouldEqual, 1)
+		})
+	})
+
+	Convey("When caching a datasource proxy with a user-supplied TLS CA", t, func() {
+		clearCache()
+		setting.SecretKey = "password"
 
-		ds.JsonData = json
+		json := simplejson.New()
+		json.Set("tlsAuthWithCACert", true)
 
-		tlsCaCert, _ := util.Encrypt([]byte(caCert), "password")
-		tlsClientCert, _ := util.Encrypt([]byte(clientCert), "password")
-		tlsClientKey, _ := util.Encrypt([]byte(clientKey), "password")
+		tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
+		So(err, ShouldBeNil)
 
-		ds.SecureJsonData = map[string][]byte{
-			"tlsCACert":     tlsCaCert,
-			"tlsClientCert": tlsClientCert,
-			"tlsClientKey":  tlsClientKey,
+		ds := DataSource{
+			Id:             1,
+			Url:            "http://k8s:8001",
+			Type:           "Kubernetes",
+			JsonData:       json,
+			SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
 		}
-		ds.Updated = t.Add(-1 * time.Minute)
 
-		transport, err = ds.GetHttpTransport()
+		tr, err := ds.GetHttpTransport()
 		So(err, ShouldBeNil)
 
-		Convey("Should add cert", func() {
-			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
-			So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
+		Convey("Should verify TLS by default", func() {
+			So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
+		})
+		Convey("Should have a TLS CA configured", func() {
+			So(len(tr.TLSClientConfig.RootCAs.Subjects()), ShouldEqual, 1)
 		})
+	})
 
-		ds.JsonData = nil
-		ds.SecureJsonData = map[string][]byte{}
-		ds.Updated = t
+	Convey("When caching a datasource proxy when user skips TLS verification", t, func() {
+		clearCache()
+
+		json := simplejson.New()
+		json.Set("tlsSkipVerify", true)
+
+		ds := DataSource{
+			Id:       1,
+			Url:      "http://k8s:8001",
+			Type:     "Kubernetes",
+			JsonData: json,
+		}
 
-		transport, err = ds.GetHttpTransport()
+		tr, err := ds.GetHttpTransport()
 		So(err, ShouldBeNil)
 
-		Convey("Should remove cert", func() {
-			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
-			So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
+		Convey("Should skip TLS verification", func() {
+			So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
 		})
 	})
 }
@@ -115,7 +194,8 @@ FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
 3lb92xM=
 -----END CERTIFICATE-----`
 
-const clientCert string = `-----BEGIN CERTIFICATE-----
+const clientCert string = `
+-----BEGIN CERTIFICATE-----
 MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
 YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
 GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD

+ 44 - 48
public/app/features/plugins/partials/ds_http_settings.html

@@ -1,23 +1,23 @@
 
 
 <div class="gf-form-group">
-  <h3 class="page-heading">Http settings</h3>
+  <h3 class="page-heading">HTTP settings</h3>
   <div class="gf-form-group">
     <div class="gf-form-inline">
       <div class="gf-form max-width-30">
-        <span class="gf-form-label width-7">Url</span>
+        <span class="gf-form-label width-7">URL</span>
         <input class="gf-form-input" type="text"
               ng-model='current.url' placeholder="{{suggestUrl}}"
               bs-typeahead="getSuggestUrls"  min-length="0"
               ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
         <info-popover mode="right-absolute">
-          <p>Specify a complete HTTP url (for example http://your_server:8080)</p>
+          <p>Specify a complete HTTP URL (for example http://your_server:8080)</p>
           <span ng-show="current.access === 'direct'">
-            Your access method is <em>Direct</em>, this means the url
+            Your access method is <em>Direct</em>, this means the URL
             needs to be accessible from the browser.
           </span>
           <span ng-show="current.access === 'proxy'">
-            Your access method is currently <em>Proxy</em>, this means the url
+            Your access method is currently <em>Proxy</em>, this means the URL
             needs to be accessible from the grafana backend.
           </span>
         </info-popover>
@@ -30,7 +30,7 @@
         <div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
           <select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
           <info-popover mode="right-absolute">
-            Direct = url is used directly from browser<br>
+            Direct = URL is used directly from browser<br>
             Proxy = Grafana backend will proxy the request
           </info-popover>
         </div>
@@ -38,27 +38,21 @@
     </div>
   </div>
 
-  <h3 class="page-heading">Http Auth</h3>
+  <h3 class="page-heading">HTTP Auth</h3>
+    <div class="gf-form-group">
+      <div class="gf-form-inline">
+        <gf-form-switch class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-8" switch-class="max-width-6"></gf-form-switch>
+        <gf-form-switch class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
+      </div>
+      <div class="gf-form-inline">
+        <gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-8" checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-switch>
+        <gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Optional. Needed for self-signed TLS Certs." checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
+      </div>
+    </div>
 
-	<div class="gf-form-inline">
-		<gf-form-switch class="gf-form"
-									label="Basic Auth"
-				 checked="current.basicAuth" label-class="width-8" switch-class="max-width-6">
-		</gf-form-switch>
-		<gf-form-switch class="gf-form"
-									label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests."
-				 checked="current.withCredentials" label-class="width-11" switch-class="max-width-6">
-		</gf-form-switch>
-	</div>
-  <div class="gf-form-inline">
-    <gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
-									label="TLS Client Auth" label-class="width-8"
-				 checked="current.jsonData.tlsAuth" switch-class="max-width-6">
-		</gf-form-switch>
-    <gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
-									label="With CA Cert" tooltip="Optional. Needed for self-signed TLS Certs."
-				 checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6">
-		</gf-form-switch>
+    <div class="gf-form-inline">
+      <gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verification (Insecure)" label-class="width-16" checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-switch>
+    </div>
   </div>
 </div>
 
@@ -79,7 +73,7 @@
 	</div>
 </div>
 
-<div class="gf-form-group" ng-if="current.jsonData.tlsAuth && current.access=='proxy'">
+<div class="gf-form-group" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
   <div class="gf-form">
     <h6>TLS Auth Details</h6>
     <info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
@@ -100,29 +94,31 @@
     </div>
   </div>
 
-  <div class="gf-form-inline">
-    <div class="gf-form gf-form--v-stretch">
-      <label class="gf-form-label width-7">Client Cert</label>
-    </div>
-    <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
-      <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
-    </div>
-    <div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
-      <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
-      <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
+  <div ng-if="current.jsonData.tlsAuth">
+    <div class="gf-form-inline">
+      <div class="gf-form gf-form--v-stretch">
+        <label class="gf-form-label width-7">Client Cert</label>
+      </div>
+      <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
+        <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
+      </div>
+      <div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
+        <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
+        <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
+      </div>
     </div>
-  </div>
 
-  <div class="gf-form-inline">
-    <div class="gf-form gf-form--v-stretch">
-      <label class="gf-form-label width-7">Client Key</label>
-    </div>
-    <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
-      <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
-    </div>
-    <div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
-      <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
-      <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
+    <div class="gf-form-inline">
+      <div class="gf-form gf-form--v-stretch">
+        <label class="gf-form-label width-7">Client Key</label>
+      </div>
+      <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
+        <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
+      </div>
+      <div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
+        <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
+        <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
+      </div>
     </div>
   </div>
 </div>