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) 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..803d966ee18 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" @@ -17,14 +18,45 @@ 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, + } + + 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 + + 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 + } + } + + cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["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 +160,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/dataproxy_test.go b/pkg/api/dataproxy_test.go index 0e561c726e1..5e6e62de0a3 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,102 @@ 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) + json.Set("tlsAuthWithCACert", true) + ds := m.DataSource{ + Url: "htttp://k8s:8001", + 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"), + }, + } + 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 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 +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 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 170a5a868fc..9f8ae329fec 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -64,22 +64,31 @@ 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"` - 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"` + 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 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/datasource.go b/pkg/models/datasource.go index 883cc3a90bd..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" ) @@ -46,6 +47,7 @@ type DataSource struct { WithCredentials bool IsDefault bool JsonData *simplejson.Json + SecureJsonData securejsondata.SecureJsonData Created time.Time Updated time.Time @@ -77,19 +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"` - 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:"-"` @@ -98,19 +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"` - 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/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) } // --------------------- diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 0e6219785bd..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" @@ -82,6 +83,7 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error { BasicAuthPassword: cmd.BasicAuthPassword, WithCredentials: cmd.WithCredentials, JsonData: cmd.JsonData, + SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData), Created: time.Now(), Updated: time.Now(), } @@ -128,6 +130,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { BasicAuthPassword: cmd.BasicAuthPassword, WithCredentials: cmd.WithCredentials, JsonData: cmd.JsonData, + SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData), Updated: time.Now(), } diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go index 90f7dac85e6..396f80df99f 100644 --- a/pkg/services/sqlstore/migrations/datasource_mig.go +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -101,4 +101,9 @@ func addDataSourceMigration(mg *Migrator) { mg.AddMigration("Add column with_credentials", NewAddColumnMigration(tableV2, &Column{ Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0", })) + + // 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, + })) } diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index 0022f6e6c19..6c116eef193 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -1,56 +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 @@ -58,7 +71,7 @@
-
+
Password @@ -66,3 +79,51 @@
+
+
+
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 {