From c9b2c694f12a764a1da43f35e90d3fbe028a209a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 16 Nov 2016 09:54:26 +0100 Subject: [PATCH] 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, })) }