From 56b7e2dfaf0a38161a7f62490505d2ba5a39d113 Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Wed, 24 Aug 2016 23:43:25 -0400 Subject: [PATCH 1/8] Added support for TLS client auth for datasource proxies (#5801) --- pkg/api/app_routes.go | 17 ++++++++- pkg/api/dataproxy.go | 35 ++++++++++++++----- pkg/api/datasources.go | 4 +++ pkg/api/dtos/models.go | 3 ++ pkg/models/datasource.go | 9 +++++ pkg/services/sqlstore/datasource.go | 7 ++++ .../sqlstore/migrations/datasource_mig.go | 11 ++++++ .../plugins/partials/ds_http_settings.html | 18 ++++++++++ 8 files changed, 94 insertions(+), 10 deletions(-) diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 7923b0475a3..aee67ac8478 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -1,6 +1,11 @@ package api import ( + "crypto/tls" + "net" + "net/http" + "time" + "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/api/pluginproxy" @@ -11,6 +16,16 @@ import ( "github.com/grafana/grafana/pkg/util" ) +var pluginProxyTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, +} + func InitAppPluginRoutes(r *macaron.Macaron) { for _, plugin := range plugins.Apps { for _, route := range plugin.Routes { @@ -40,7 +55,7 @@ func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler path := c.Params("*") proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId) - proxy.Transport = dataProxyTransport + proxy.Transport = pluginProxyTransport proxy.ServeHTTP(c.Resp, c.Req.Request) } } diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 34c4271ebf6..7f527ed3c74 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -17,14 +17,27 @@ import ( "github.com/grafana/grafana/pkg/util" ) -var dataProxyTransport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, +func dataProxyTransport(ds *m.DataSource) (*http.Transport, error) { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + } + + if ds.TlsAuth { + cert, err := tls.LoadX509KeyPair(ds.TlsClientCert, ds.TlsClientKey) + if err != nil { + return nil, err + } + transport.TLSClientConfig.Certificates = []tls.Certificate{cert} + } + return transport, nil } func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy { @@ -128,7 +141,11 @@ func ProxyDataSourceRequest(c *middleware.Context) { } proxy := NewReverseProxy(ds, proxyPath, targetUrl) - proxy.Transport = dataProxyTransport + proxy.Transport, err = dataProxyTransport(ds) + if err != nil { + c.JsonApiErr(400, "Unable to load TLS certificate", err) + return + } proxy.ServeHTTP(c.Resp, c.Req.Request) c.Resp.Header().Del("Set-Cookie") } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 2b9964f7a71..6d30a7dc44a 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -33,6 +33,7 @@ func GetDataSources(c *middleware.Context) { Database: ds.Database, User: ds.User, BasicAuth: ds.BasicAuth, + TlsAuth: ds.TlsAuth, IsDefault: ds.IsDefault, JsonData: ds.JsonData, } @@ -165,6 +166,9 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource { BasicAuth: ds.BasicAuth, BasicAuthUser: ds.BasicAuthUser, BasicAuthPassword: ds.BasicAuthPassword, + TlsAuth: ds.TlsAuth, + TlsClientCert: ds.TlsClientCert, + TlsClientKey: ds.TlsClientKey, WithCredentials: ds.WithCredentials, IsDefault: ds.IsDefault, JsonData: ds.JsonData, diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 170a5a868fc..d902f4a58a0 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -77,6 +77,9 @@ type DataSource struct { BasicAuth bool `json:"basicAuth"` BasicAuthUser string `json:"basicAuthUser"` BasicAuthPassword string `json:"basicAuthPassword"` + TlsAuth bool `json:"tlsAuth"` + TlsClientCert string `json:"tlsClientCert"` + TlsClientKey string `json:"tlsClientKey"` WithCredentials bool `json:"withCredentials"` IsDefault bool `json:"isDefault"` JsonData *simplejson.Json `json:"jsonData,omitempty"` diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 883cc3a90bd..c4400e9f3dd 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -43,6 +43,9 @@ type DataSource struct { BasicAuth bool BasicAuthUser string BasicAuthPassword string + TlsAuth bool + TlsClientCert string + TlsClientKey string WithCredentials bool IsDefault bool JsonData *simplejson.Json @@ -87,6 +90,9 @@ type AddDataSourceCommand struct { BasicAuth bool `json:"basicAuth"` BasicAuthUser string `json:"basicAuthUser"` BasicAuthPassword string `json:"basicAuthPassword"` + TlsAuth bool `json:"tlsAuth"` + TlsClientCert string `json:"tlsClientCert"` + TlsClientKey string `json:"tlsClientKey"` WithCredentials bool `json:"withCredentials"` IsDefault bool `json:"isDefault"` JsonData *simplejson.Json `json:"jsonData"` @@ -108,6 +114,9 @@ type UpdateDataSourceCommand struct { BasicAuth bool `json:"basicAuth"` BasicAuthUser string `json:"basicAuthUser"` BasicAuthPassword string `json:"basicAuthPassword"` + TlsAuth bool `json:"tlsAuth"` + TlsClientCert string `json:"tlsClientCert"` + TlsClientKey string `json:"tlsClientKey"` WithCredentials bool `json:"withCredentials"` IsDefault bool `json:"isDefault"` JsonData *simplejson.Json `json:"jsonData"` diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 0e6219785bd..29581c03096 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -80,6 +80,9 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error { BasicAuth: cmd.BasicAuth, BasicAuthUser: cmd.BasicAuthUser, BasicAuthPassword: cmd.BasicAuthPassword, + TlsAuth: cmd.TlsAuth, + TlsClientCert: cmd.TlsClientCert, + TlsClientKey: cmd.TlsClientKey, WithCredentials: cmd.WithCredentials, JsonData: cmd.JsonData, Created: time.Now(), @@ -126,6 +129,9 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { BasicAuth: cmd.BasicAuth, BasicAuthUser: cmd.BasicAuthUser, BasicAuthPassword: cmd.BasicAuthPassword, + TlsAuth: cmd.TlsAuth, + TlsClientCert: cmd.TlsClientCert, + TlsClientKey: cmd.TlsClientKey, WithCredentials: cmd.WithCredentials, JsonData: cmd.JsonData, Updated: time.Now(), @@ -133,6 +139,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { sess.UseBool("is_default") sess.UseBool("basic_auth") + sess.UseBool("tls_auth") sess.UseBool("with_credentials") _, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds) diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go index 90f7dac85e6..58205bbb196 100644 --- a/pkg/services/sqlstore/migrations/datasource_mig.go +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -101,4 +101,15 @@ func addDataSourceMigration(mg *Migrator) { mg.AddMigration("Add column with_credentials", NewAddColumnMigration(tableV2, &Column{ Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0", })) + + // add columns to activate TLS client auth option + mg.AddMigration("Add column tls_auth", NewAddColumnMigration(tableV2, &Column{ + Name: "tls_auth", Type: DB_Bool, Nullable: false, Default: "0", + })) + mg.AddMigration("Add column tls_client_cert", NewAddColumnMigration(tableV2, &Column{ + Name: "tls_client_cert", Type: DB_NVarchar, Length: 255, Nullable: true, + })) + mg.AddMigration("Add column tls_client_key", NewAddColumnMigration(tableV2, &Column{ + Name: "tls_client_key", Type: DB_NVarchar, Length: 255, Nullable: true, + })) } diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index 0022f6e6c19..5d980f43fdd 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -49,6 +49,10 @@ label="With Credentials" checked="current.withCredentials" switch-class="max-width-6"> + +
@@ -64,5 +68,19 @@
+ +
+ + Client Cert + + +
+ +
+ + Client Key + + +
From af07adb146d47caedad6549141cf679db8494e3f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sat, 12 Nov 2016 23:26:33 +0100 Subject: [PATCH 2/8] refactor(securejsondata): extract to class Extract from pluginsettings class so that the securejsondata type can be used in the other classes. Encrypt and decrypt functions extracted too. --- .../securejsondata/securejsondata.go | 24 +++++++++++++++++++ pkg/models/plugin_settings.go | 23 ++++-------------- 2 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 pkg/components/securejsondata/securejsondata.go diff --git a/pkg/components/securejsondata/securejsondata.go b/pkg/components/securejsondata/securejsondata.go new file mode 100644 index 00000000000..d7bec6894b0 --- /dev/null +++ b/pkg/components/securejsondata/securejsondata.go @@ -0,0 +1,24 @@ +package securejsondata + +import ( + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +type SecureJsonData map[string][]byte + +func (s SecureJsonData) Decrypt() map[string]string { + decrypted := make(map[string]string) + for key, data := range s { + decrypted[key] = string(util.Decrypt(data, setting.SecretKey)) + } + return decrypted +} + +func GetEncryptedJsonData(sjd map[string]string) SecureJsonData { + encrypted := make(SecureJsonData) + for key, data := range sjd { + encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey) + } + return encrypted +} diff --git a/pkg/models/plugin_settings.go b/pkg/models/plugin_settings.go index 21c48411797..bbfbaabd8c6 100644 --- a/pkg/models/plugin_settings.go +++ b/pkg/models/plugin_settings.go @@ -4,8 +4,7 @@ import ( "errors" "time" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/components/securejsondata" ) var ( @@ -19,23 +18,13 @@ type PluginSetting struct { Enabled bool Pinned bool JsonData map[string]interface{} - SecureJsonData SecureJsonData + SecureJsonData securejsondata.SecureJsonData PluginVersion string Created time.Time Updated time.Time } -type SecureJsonData map[string][]byte - -func (s SecureJsonData) Decrypt() map[string]string { - decrypted := make(map[string]string) - for key, data := range s { - decrypted[key] = string(util.Decrypt(data, setting.SecretKey)) - } - return decrypted -} - // ---------------------- // COMMANDS @@ -58,12 +47,8 @@ type UpdatePluginSettingVersionCmd struct { OrgId int64 `json:"-"` } -func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData { - encrypted := make(SecureJsonData) - for key, data := range cmd.SecureJsonData { - encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey) - } - return encrypted +func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() securejsondata.SecureJsonData { + return securejsondata.GetEncryptedJsonData(cmd.SecureJsonData) } // --------------------- From c9b2c694f12a764a1da43f35e90d3fbe028a209a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 16 Nov 2016 09:54:26 +0100 Subject: [PATCH 3/8] refactor(dataproxy): TLS Client Auth Use a SecureJsonData field for TLS Client Auth instead of 3 new db fields. Same model as used for PluginSettings. Saves and encrypts the pem file content rather than just saving the paths to the cert and key. This allows for uploading from the Edit Datasource page in Grafana. --- pkg/api/dataproxy.go | 17 +++- pkg/api/dataproxy_test.go | 84 ++++++++++++++++++- pkg/api/datasources.go | 4 - pkg/api/dtos/models.go | 36 ++++---- pkg/models/datasource.go | 65 +++++++------- pkg/services/sqlstore/datasource.go | 10 +-- .../sqlstore/migrations/datasource_mig.go | 12 +-- 7 files changed, 149 insertions(+), 79 deletions(-) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 7f527ed3c74..8ab80b9e18e 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -17,7 +17,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func dataProxyTransport(ds *m.DataSource) (*http.Transport, error) { +func DataProxyTransport(ds *m.DataSource) (*http.Transport, error) { transport := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, @@ -30,8 +30,17 @@ func dataProxyTransport(ds *m.DataSource) (*http.Transport, error) { TLSHandshakeTimeout: 10 * time.Second, } - if ds.TlsAuth { - cert, err := tls.LoadX509KeyPair(ds.TlsClientCert, ds.TlsClientKey) + var tlsAuth bool + var err error + if ds.JsonData != nil { + tlsAuth, err = ds.JsonData.Get("tlsAuth").Bool() + } + + if err == nil && tlsAuth { + transport.TLSClientConfig.InsecureSkipVerify = false + + decrypted := ds.SecureJsonData.Decrypt() + cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"])) if err != nil { return nil, err } @@ -141,7 +150,7 @@ func ProxyDataSourceRequest(c *middleware.Context) { } proxy := NewReverseProxy(ds, proxyPath, targetUrl) - proxy.Transport, err = dataProxyTransport(ds) + proxy.Transport, err = DataProxyTransport(ds) if err != nil { c.JsonApiErr(400, "Unable to load TLS certificate", err) return diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go index 0e561c726e1..d421eb74af5 100644 --- a/pkg/api/dataproxy_test.go +++ b/pkg/api/dataproxy_test.go @@ -7,15 +7,24 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) func TestDataSourceProxy(t *testing.T) { Convey("When getting graphite datasource proxy", t, func() { ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} - targetUrl, _ := url.Parse(ds.Url) + targetUrl, err := url.Parse(ds.Url) proxy := NewReverseProxy(&ds, "/render", targetUrl) + proxy.Transport, err = DataProxyTransport(&ds) + So(err, ShouldBeNil) + + transport, ok := proxy.Transport.(*http.Transport) + So(ok, ShouldBeTrue) + So(transport.TLSClientConfig.InsecureSkipVerify, ShouldBeTrue) requestUrl, _ := url.Parse("http://grafana.com/sub") req := http.Request{URL: requestUrl} @@ -54,7 +63,80 @@ func TestDataSourceProxy(t *testing.T) { So(queryVals["u"][0], ShouldEqual, "user") So(queryVals["p"][0], ShouldEqual, "password") }) + }) + Convey("When getting kubernetes datasource proxy", t, func() { + setting.SecretKey = "password" + + json := simplejson.New() + json.Set("tlsAuth", true) + ds := m.DataSource{ + Url: "htttp://k8s:8001", + Type: "Kubernetes", + JsonData: json, + SecureJsonData: map[string][]byte{ + "tlsClientCert": util.Encrypt([]byte(clientCert), "password"), + "tlsClientKey": util.Encrypt([]byte(clientKey), "password"), + }, + } + targetUrl, err := url.Parse(ds.Url) + proxy := NewReverseProxy(&ds, "", targetUrl) + proxy.Transport, err = DataProxyTransport(&ds) + So(err, ShouldBeNil) + + transport, ok := proxy.Transport.(*http.Transport) + + Convey("Should add cert", func() { + So(ok, ShouldBeTrue) + So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) + So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1) + }) }) } + +const clientCert string = `-----BEGIN CERTIFICATE----- +MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj +YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w +GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2 +FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b +Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo +GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8 +SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4 +YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP +ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw +AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q +4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe +58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5 +llG/Sw5+FquFuChaA6l5KWy7F3bQyA== +-----END CERTIFICATE-----` + +const clientKey string = `-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV +u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn +Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega +0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI +LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi +dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs +Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk +CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x +64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM +8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh +WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf +vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz +k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs +DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35 +aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ +Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo +jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01 +hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0 +M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8 +v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX +xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL +Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0 +Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD +FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD ++VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg= +-----END RSA PRIVATE KEY-----` diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 6d30a7dc44a..2b9964f7a71 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -33,7 +33,6 @@ func GetDataSources(c *middleware.Context) { Database: ds.Database, User: ds.User, BasicAuth: ds.BasicAuth, - TlsAuth: ds.TlsAuth, IsDefault: ds.IsDefault, JsonData: ds.JsonData, } @@ -166,9 +165,6 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource { BasicAuth: ds.BasicAuth, BasicAuthUser: ds.BasicAuthUser, BasicAuthPassword: ds.BasicAuthPassword, - TlsAuth: ds.TlsAuth, - TlsClientCert: ds.TlsClientCert, - TlsClientKey: ds.TlsClientKey, WithCredentials: ds.WithCredentials, IsDefault: ds.IsDefault, JsonData: ds.JsonData, diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index d902f4a58a0..153468c582c 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -64,25 +64,23 @@ type DashboardRedirect struct { } type DataSource struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Type string `json:"type"` - TypeLogoUrl string `json:"typeLogoUrl"` - Access m.DsAccess `json:"access"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - TlsAuth bool `json:"tlsAuth"` - TlsClientCert string `json:"tlsClientCert"` - TlsClientKey string `json:"tlsClientKey"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData *simplejson.Json `json:"jsonData,omitempty"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Type string `json:"type"` + TypeLogoUrl string `json:"typeLogoUrl"` + Access m.DsAccess `json:"access"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData,omitempty"` + SecureJsonData map[string]string `json:"secureJsonData,omitempty"` } type DataSourceList []DataSource diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index c4400e9f3dd..4a90edd9bfd 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -4,6 +4,7 @@ import ( "errors" "time" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" ) @@ -43,12 +44,10 @@ type DataSource struct { BasicAuth bool BasicAuthUser string BasicAuthPassword string - TlsAuth bool - TlsClientCert string - TlsClientKey string WithCredentials bool IsDefault bool JsonData *simplejson.Json + SecureJsonData securejsondata.SecureJsonData Created time.Time Updated time.Time @@ -80,22 +79,20 @@ func IsKnownDataSourcePlugin(dsType string) bool { // Also acts as api DTO type AddDataSourceCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Access DsAccess `json:"access" binding:"Required"` - Url string `json:"url"` - Password string `json:"password"` - Database string `json:"database"` - User string `json:"user"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - TlsAuth bool `json:"tlsAuth"` - TlsClientCert string `json:"tlsClientCert"` - TlsClientKey string `json:"tlsClientKey"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData *simplejson.Json `json:"jsonData"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Access DsAccess `json:"access" binding:"Required"` + Url string `json:"url"` + Password string `json:"password"` + Database string `json:"database"` + User string `json:"user"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData"` + SecureJsonData map[string]string `json:"secureJsonData"` OrgId int64 `json:"-"` @@ -104,22 +101,20 @@ type AddDataSourceCommand struct { // Also acts as api DTO type UpdateDataSourceCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Access DsAccess `json:"access" binding:"Required"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - TlsAuth bool `json:"tlsAuth"` - TlsClientCert string `json:"tlsClientCert"` - TlsClientKey string `json:"tlsClientKey"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData *simplejson.Json `json:"jsonData"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Access DsAccess `json:"access" binding:"Required"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData"` + SecureJsonData map[string]string `json:"secureJsonData"` OrgId int64 `json:"-"` Id int64 `json:"-"` diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 29581c03096..57abaf35083 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -4,6 +4,7 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/securejsondata" m "github.com/grafana/grafana/pkg/models" "github.com/go-xorm/xorm" @@ -80,11 +81,9 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error { BasicAuth: cmd.BasicAuth, BasicAuthUser: cmd.BasicAuthUser, BasicAuthPassword: cmd.BasicAuthPassword, - TlsAuth: cmd.TlsAuth, - TlsClientCert: cmd.TlsClientCert, - TlsClientKey: cmd.TlsClientKey, WithCredentials: cmd.WithCredentials, JsonData: cmd.JsonData, + SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData), Created: time.Now(), Updated: time.Now(), } @@ -129,17 +128,14 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { BasicAuth: cmd.BasicAuth, BasicAuthUser: cmd.BasicAuthUser, BasicAuthPassword: cmd.BasicAuthPassword, - TlsAuth: cmd.TlsAuth, - TlsClientCert: cmd.TlsClientCert, - TlsClientKey: cmd.TlsClientKey, WithCredentials: cmd.WithCredentials, JsonData: cmd.JsonData, + SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData), Updated: time.Now(), } sess.UseBool("is_default") sess.UseBool("basic_auth") - sess.UseBool("tls_auth") sess.UseBool("with_credentials") _, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds) diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go index 58205bbb196..396f80df99f 100644 --- a/pkg/services/sqlstore/migrations/datasource_mig.go +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -102,14 +102,8 @@ func addDataSourceMigration(mg *Migrator) { Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0", })) - // add columns to activate TLS client auth option - mg.AddMigration("Add column tls_auth", NewAddColumnMigration(tableV2, &Column{ - Name: "tls_auth", Type: DB_Bool, Nullable: false, Default: "0", - })) - mg.AddMigration("Add column tls_client_cert", NewAddColumnMigration(tableV2, &Column{ - Name: "tls_client_cert", Type: DB_NVarchar, Length: 255, Nullable: true, - })) - mg.AddMigration("Add column tls_client_key", NewAddColumnMigration(tableV2, &Column{ - Name: "tls_client_key", Type: DB_NVarchar, Length: 255, Nullable: true, + // add column that can store TLS client auth data + mg.AddMigration("Add secure json data column", NewAddColumnMigration(tableV2, &Column{ + Name: "secure_json_data", Type: DB_Text, Nullable: true, })) } From 387f8cc0c626bfdb4aa44989c698806b8208eefe Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 16 Nov 2016 13:50:56 +0100 Subject: [PATCH 4/8] feat(dataproxy): TLS CA Cert for self-signed certs For self-signed TLS Certificates, authentication with InsecureSkipVerify set to false then this error will occur: x509: certificate signed by unknown authority The solution is to allow the user to upload the CA cert as well. --- pkg/api/dataproxy.go | 10 ++++++++++ pkg/api/dataproxy_test.go | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 8ab80b9e18e..4f2a2ebfce4 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -2,6 +2,7 @@ package api import ( "crypto/tls" + "crypto/x509" "net" "net/http" "net/http/httputil" @@ -40,6 +41,15 @@ func DataProxyTransport(ds *m.DataSource) (*http.Transport, error) { transport.TLSClientConfig.InsecureSkipVerify = false decrypted := ds.SecureJsonData.Decrypt() + + if len(decrypted["tlsCACert"]) > 0 { + caPool := x509.NewCertPool() + ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"])) + if ok { + transport.TLSClientConfig.RootCAs = caPool + } + } + cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"])) if err != nil { return nil, err diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go index d421eb74af5..837a3d2c59c 100644 --- a/pkg/api/dataproxy_test.go +++ b/pkg/api/dataproxy_test.go @@ -75,6 +75,7 @@ func TestDataSourceProxy(t *testing.T) { Type: "Kubernetes", JsonData: json, SecureJsonData: map[string][]byte{ + "tlsCACert": util.Encrypt([]byte(caCert), "password"), "tlsClientCert": util.Encrypt([]byte(clientCert), "password"), "tlsClientKey": util.Encrypt([]byte(clientKey), "password"), }, @@ -95,6 +96,26 @@ func TestDataSourceProxy(t *testing.T) { } +const caCert string = `-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV +BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda +MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg +QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4 +4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d +1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN +iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ +0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx +2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5 +VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9 +I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB +CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7 +adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k +MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh +FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n +3lb92xM= +-----END CERTIFICATE-----` + const clientCert string = `-----BEGIN CERTIFICATE----- MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w From 2893b25a0553f0c65f8ebf81947a2497723652dc Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 18 Nov 2016 16:44:59 +0100 Subject: [PATCH 5/8] feat(datasources): allow updating of tls certs The tls files are stored as a json blob in the SecureJsonData field. To update one file, the json has to be fetched from the db first and then updated with the cert file that has been changed. Also, a change to the dto with flags that are used in the frontend to show if a file has been uploaded. For example, if tlsClientKeySet is set to true then means a file for the client key is saved in the db. This is to avoid sending any private data to the frontend. --- pkg/api/datasources.go | 59 +++++++++++++++++++++++++++++++++++++----- pkg/api/dtos/models.go | 8 ++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 2b9964f7a71..87c743c9ce6 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -104,17 +104,56 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) { c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id}) } -func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { +func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":id") - err := bus.Dispatch(&cmd) + err := fillWithSecureJsonData(&cmd) if err != nil { - c.JsonApiErr(500, "Failed to update datasource", err) - return + return ApiError(500, "Failed to update datasource", err) } - c.JsonOK("Datasource updated") + err = bus.Dispatch(&cmd) + if err != nil { + return ApiError(500, "Failed to update datasource", err) + } + + return Json(200, "Datasource updated") +} + +func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { + if len(cmd.SecureJsonData) == 0 { + return nil + } + + ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId) + + if err != nil { + return err + } + secureJsonData := ds.SecureJsonData.Decrypt() + + for k, v := range secureJsonData { + + if _, ok := cmd.SecureJsonData[k]; !ok { + cmd.SecureJsonData[k] = v + } + } + + return nil +} + +func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) { + query := m.GetDataSourceByIdQuery{ + Id: id, + OrgId: orgId, + } + + if err := bus.Dispatch(&query); err != nil { + return nil, err + } + + return query.Result, nil } // Get /api/datasources/name/:name @@ -152,7 +191,7 @@ func GetDataSourceIdByName(c *middleware.Context) Response { } func convertModelToDtos(ds *m.DataSource) dtos.DataSource { - return dtos.DataSource{ + dto := dtos.DataSource{ Id: ds.Id, OrgId: ds.OrgId, Name: ds.Name, @@ -169,4 +208,12 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource { IsDefault: ds.IsDefault, JsonData: ds.JsonData, } + + if len(ds.SecureJsonData) > 0 { + dto.TLSAuth.CACertSet = len(ds.SecureJsonData["tlsCACert"]) > 0 + dto.TLSAuth.ClientCertSet = len(ds.SecureJsonData["tlsClientCert"]) > 0 + dto.TLSAuth.ClientKeySet = len(ds.SecureJsonData["tlsClientKey"]) > 0 + } + + return dto } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 153468c582c..9f8ae329fec 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -81,6 +81,14 @@ type DataSource struct { IsDefault bool `json:"isDefault"` JsonData *simplejson.Json `json:"jsonData,omitempty"` SecureJsonData map[string]string `json:"secureJsonData,omitempty"` + TLSAuth TLSAuth `json:"tlsAuth,omitempty"` +} + +// TLSAuth is used to show if TLS certs have been uploaded already +type TLSAuth struct { + CACertSet bool `json:"tlsCACertSet"` + ClientCertSet bool `json:"tlsClientCertSet"` + ClientKeySet bool `json:"tlsClientKeySet"` } type DataSourceList []DataSource From b6b53c0f4b102ad4d1a5a649b6fc907cd782f9fd Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 18 Nov 2016 18:13:13 +0100 Subject: [PATCH 6/8] fix(dataproxy): test with CA Cert --- pkg/api/dataproxy_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go index 837a3d2c59c..5e6e62de0a3 100644 --- a/pkg/api/dataproxy_test.go +++ b/pkg/api/dataproxy_test.go @@ -70,6 +70,7 @@ func TestDataSourceProxy(t *testing.T) { json := simplejson.New() json.Set("tlsAuth", true) + json.Set("tlsAuthWithCACert", true) ds := m.DataSource{ Url: "htttp://k8s:8001", Type: "Kubernetes", From 0618122bcd18525cfdb724f98832f9970e4a5a00 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 18 Nov 2016 16:53:07 +0100 Subject: [PATCH 7/8] feat(httpsettings): add tls auth option - Three text areas where the user can paste in the CA Cert (optional), Client Cert and Client Key. - Tooltips for Auth checkboxes with brief descriptions of what With Credentials and With CA Cert are. - Adds popover for TLS Auth header too. - Aligns gf-form elements as labels and checkboxes were not aligned before. - Makes CA Cert optional as it is only needed for self-signed certs. --- pkg/api/dataproxy.go | 10 +- .../plugins/partials/ds_http_settings.html | 163 +++++++++++------- public/sass/components/_gf-form.scss | 8 + 3 files changed, 116 insertions(+), 65 deletions(-) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 4f2a2ebfce4..803d966ee18 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -31,18 +31,18 @@ func DataProxyTransport(ds *m.DataSource) (*http.Transport, error) { TLSHandshakeTimeout: 10 * time.Second, } - var tlsAuth bool - var err error + var tlsAuth, tlsAuthWithCACert bool if ds.JsonData != nil { - tlsAuth, err = ds.JsonData.Get("tlsAuth").Bool() + tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) } - if err == nil && tlsAuth { + if tlsAuth { transport.TLSClientConfig.InsecureSkipVerify = false decrypted := ds.SecureJsonData.Decrypt() - if len(decrypted["tlsCACert"]) > 0 { + if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 { caPool := x509.NewCertPool() ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"])) if ok { diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index 5d980f43fdd..6c116eef193 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -1,60 +1,69 @@
-

Http settings

+

Http settings

+
+
+
+ Url + + +

Specify a complete HTTP url (for example http://your_server:8080)

+ + Your access method is Direct, this means the url + needs to be accessable from the browser. + + + Your access method is currently Proxy, this means the url + needs to be accessable from the grafana backend. + +
+
+
+ +
+
+ Access +
+ + + Direct = url is used directly from browser
+ Proxy = Grafana backend will proxy the request +
+
+
+
+
+ +

Http Auth

-
- Url - - -

Specify a complete HTTP url (for example http://your_server:8080)

- - Your access method is Direct, this means the url - needs to be accessable from the browser. - - - Your access method is currently Proxy, this means the url - needs to be accessable from the grafana backend. - -
-
-
- -
-
- Access -
- - - Direct = url is used directly from browser
- Proxy = Grafana backend will proxy the request -
-
-
-
- -
-
- -
+ checked="current.basicAuth" label-class="width-8" switch-class="max-width-6"> - - + 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">
+
+ + + + +
+
+
+
Basic Auth Details
User @@ -62,25 +71,59 @@
-
+
Password
- -
- - Client Cert - - -
- -
- - Client Key - - -
+
+ +
+
+
TLS Auth Details
+ TLS Certs are encrypted and stored in the Grafana database. +
+
+
+
+ +
+
+ +
+ +
+ + reset +
+
+
+ +
+
+ +
+
+ +
+
+ + reset +
+
+ +
+
+ +
+
+ +
+
+ + reset +
+
diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index c22418d70da..42498c47ac3 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -79,6 +79,10 @@ $gf-form-margin: 0.25rem; } } +.gf-form-textarea { + max-width: 650px; +} + .gf-form-input { display: block; width: 100%; @@ -249,6 +253,10 @@ $gf-form-margin: 0.25rem; &--right-normal { float: right; } + + &--header { + margin-bottom: $gf-form-margin + } } select.gf-form-input ~ .gf-form-help-icon { From ad8f06f87f95917f6c938b9531d5dd6773a72ad7 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 24 Nov 2016 13:57:19 +0100 Subject: [PATCH 8/8] feat(changelog): adds TLS Client auth --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a28e589f6f..b0e209cbc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ -# 4.0-stable (unrelased) +# 4.0-stable (unreleased) ### Bugfixes * **Server-side rendering**: Fixed address used when rendering panel via phantomjs and using non default http_addr config [#6660](https://github.com/grafana/grafana/issues/6660) * **Graph panel**: Fixed graph panel tooltip sort order issue [#6648](https://github.com/grafana/grafana/issues/6648) * **Unsaved changes**: You now navigate to the intended page after saving in the unsaved changes dialog [#6675](https://github.com/grafana/grafana/issues/6675) +* **TLS Client Auth**: Support for TLS client authentication for datasource proxies [#2316](https://github.com/grafana/grafana/issues/2316) # 4.0-beta2 (2016-11-21)