瀏覽代碼

Security: Store datasource passwords encrypted in secureJsonData (#16175)

* Store passwords in secureJsonData

* Revert unnecessary refactors

* Fix for nil jsonSecureData value

* Remove copied encryption code from migration

* Fix wrong field reference

* Remove migration and provisioning changes

* Use password getters in datasource proxy

* Refactor password handling in datasource configs

* Add provisioning warnings

* Update documentation

* Remove migration command, moved to separate PR

* Remove unused code

* Set the upgrade version

* Remove unused code

* Remove double reference
Andrej Ocenas 6 年之前
父節點
當前提交
66f6e16916
共有 30 個文件被更改,包括 352 次插入85 次删除
  1. 6 3
      devenv/datasources.yaml
  2. 8 4
      docs/sources/administration/provisioning.md
  3. 12 1
      docs/sources/installation/upgrading.md
  4. 5 5
      pkg/api/frontendsettings.go
  5. 3 3
      pkg/api/pluginproxy/ds_proxy.go
  6. 108 6
      pkg/api/pluginproxy/ds_proxy_test.go
  7. 18 0
      pkg/components/securejsondata/securejsondata.go
  8. 20 0
      pkg/models/datasource.go
  9. 2 2
      pkg/services/provisioning/datasources/config_reader.go
  10. 9 1
      pkg/services/provisioning/datasources/types.go
  11. 4 0
      pkg/services/sqlstore/datasource.go
  12. 2 2
      pkg/tsdb/elasticsearch/client/client.go
  13. 1 1
      pkg/tsdb/graphite/graphite.go
  14. 2 2
      pkg/tsdb/influxdb/influxdb.go
  15. 1 9
      pkg/tsdb/mssql/mssql.go
  16. 1 1
      pkg/tsdb/mysql/mysql.go
  17. 1 1
      pkg/tsdb/opentsdb/opentsdb.go
  18. 1 9
      pkg/tsdb/postgres/postgres.go
  19. 1 1
      pkg/tsdb/prometheus/prometheus.go
  20. 1 0
      public/app/core/angular_wrappers.ts
  21. 8 2
      public/app/features/datasources/partials/http_settings.html
  22. 4 0
      public/app/features/datasources/settings/HttpSettingsCtrl.ts
  23. 35 0
      public/app/features/datasources/utils/passwordHandlers.test.ts
  24. 39 0
      public/app/features/datasources/utils/passwordHandlers.ts
  25. 13 0
      public/app/plugins/datasource/influxdb/module.ts
  26. 8 3
      public/app/plugins/datasource/influxdb/partials/config.html
  27. 9 13
      public/app/plugins/datasource/mssql/config_ctrl.ts
  28. 13 0
      public/app/plugins/datasource/mysql/module.ts
  29. 8 3
      public/app/plugins/datasource/mysql/partials/config.html
  30. 9 13
      public/app/plugins/datasource/postgres/config_ctrl.ts

+ 6 - 3
devenv/datasources.yaml

@@ -22,10 +22,11 @@ datasources:
     access: proxy
     access: proxy
     database: site
     database: site
     user: grafana
     user: grafana
-    password: grafana
     url: http://localhost:8086
     url: http://localhost:8086
     jsonData:
     jsonData:
       timeInterval: "15s"
       timeInterval: "15s"
+    secureJsonData:
+      password: grafana
 
 
   - name: gdev-opentsdb
   - name: gdev-opentsdb
     type: opentsdb
     type: opentsdb
@@ -110,14 +111,16 @@ datasources:
     url: localhost:3306
     url: localhost:3306
     database: grafana
     database: grafana
     user: grafana
     user: grafana
-    password: password
+    secureJsonData:
+      password: password
 
 
   - name: gdev-mysql-ds-tests
   - name: gdev-mysql-ds-tests
     type: mysql
     type: mysql
     url: localhost:3306
     url: localhost:3306
     database: grafana_ds_tests
     database: grafana_ds_tests
     user: grafana
     user: grafana
-    password: password
+    secureJsonData:
+      password: password
 
 
   - name: gdev-mssql
   - name: gdev-mssql
     type: mssql
     type: mssql

+ 8 - 4
docs/sources/administration/provisioning.md

@@ -107,7 +107,7 @@ datasources:
   orgId: 1
   orgId: 1
   # <string> url
   # <string> url
   url: http://localhost:8080
   url: http://localhost:8080
-  # <string> database password, if used
+  # <string> Deprecated, use secureJsonData.password
   password:
   password:
   # <string> database user, if used
   # <string> database user, if used
   user:
   user:
@@ -117,7 +117,7 @@ datasources:
   basicAuth:
   basicAuth:
   # <string> basic auth username
   # <string> basic auth username
   basicAuthUser:
   basicAuthUser:
-  # <string> basic auth password
+  # <string> Deprecated, use secureJsonData.basicAuthPassword
   basicAuthPassword:
   basicAuthPassword:
   # <bool> enable/disable with credentials headers
   # <bool> enable/disable with credentials headers
   withCredentials:
   withCredentials:
@@ -133,6 +133,10 @@ datasources:
     tlsCACert: "..."
     tlsCACert: "..."
     tlsClientCert: "..."
     tlsClientCert: "..."
     tlsClientKey: "..."
     tlsClientKey: "..."
+    # <string> database password, if used
+    password:
+    # <string> basic auth password
+    basicAuthPassword:
   version: 1
   version: 1
   # <bool> allow users to edit datasources from the UI.
   # <bool> allow users to edit datasources from the UI.
   editable: false
   editable: false
@@ -184,8 +188,8 @@ Secure json data is a map of settings that will be encrypted with [secret key](/
 | tlsCACert | string | *All* |CA cert for out going requests |
 | tlsCACert | string | *All* |CA cert for out going requests |
 | tlsClientCert | string | *All* |TLS Client cert for outgoing requests |
 | tlsClientCert | string | *All* |TLS Client cert for outgoing requests |
 | tlsClientKey | string | *All* |TLS Client key for outgoing requests |
 | tlsClientKey | string | *All* |TLS Client key for outgoing requests |
-| password | string | PostgreSQL | password |
-| user | string | PostgreSQL | user |
+| password | string | *All* | password |
+| basicAuthPassword | string | *All* | password for basic authentication |
 | accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch |
 | accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch |
 | secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch |
 | secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch |
 
 

+ 12 - 1
docs/sources/installation/upgrading.md

@@ -123,7 +123,18 @@ If you're using systemd and have a large amount of annotations consider temporar
 If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitized HTML.
 If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitized HTML.
 Read more [here](/installation/configuration/#disable-sanitize-html) about this new setting.
 Read more [here](/installation/configuration/#disable-sanitize-html) about this new setting.
 
 
-### Authentication and security
+## Upgrading to v6.2
+
+Datasources store passwords and basic auth passwords in secureJsonData encrypted by default. Existing datasource 
+will keep working with unencrypted passwords. If you want to migrate to encrypted storage for your existing datasources
+you can do that by:
+- For datasources created through UI, you need to go to datasource config, re enter the password or basic auth
+password and save the datasource.
+- For datasources created by provisioning, you need to update your config file and use secureJsonData.password or
+secureJsonData.basicAuthPassword field. See [provisioning docs](/administration/provisioning) for example of current
+configuration.
+
+## Authentication and security
 
 
 If your using Grafana's builtin, LDAP (without Auth Proxy) or OAuth authentication all users will be required to login upon the next visit after the upgrade.
 If your using Grafana's builtin, LDAP (without Auth Proxy) or OAuth authentication all users will be required to login upon the next visit after the upgrade.
 
 

+ 5 - 5
pkg/api/frontendsettings.go

@@ -1,6 +1,7 @@
 package api
 package api
 
 
 import (
 import (
+	"github.com/grafana/grafana/pkg/util"
 	"strconv"
 	"strconv"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
@@ -8,9 +9,9 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
-	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
+// getFrontendSettingsMap returns a json object with all the settings needed for front end initialisation.
 func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 	orgDataSources := make([]*m.DataSource, 0)
 	orgDataSources := make([]*m.DataSource, 0)
 
 
@@ -92,7 +93,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
 
 
 		if ds.Access == m.DS_ACCESS_DIRECT {
 		if ds.Access == m.DS_ACCESS_DIRECT {
 			if ds.BasicAuth {
 			if ds.BasicAuth {
-				dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
+				dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.DecryptedBasicAuthPassword())
 			}
 			}
 			if ds.WithCredentials {
 			if ds.WithCredentials {
 				dsMap["withCredentials"] = ds.WithCredentials
 				dsMap["withCredentials"] = ds.WithCredentials
@@ -100,14 +101,13 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
 
 
 			if ds.Type == m.DS_INFLUXDB_08 {
 			if ds.Type == m.DS_INFLUXDB_08 {
 				dsMap["username"] = ds.User
 				dsMap["username"] = ds.User
-				dsMap["password"] = ds.Password
+				dsMap["password"] = ds.DecryptedPassword()
 				dsMap["url"] = url + "/db/" + ds.Database
 				dsMap["url"] = url + "/db/" + ds.Database
 			}
 			}
 
 
 			if ds.Type == m.DS_INFLUXDB {
 			if ds.Type == m.DS_INFLUXDB {
 				dsMap["username"] = ds.User
 				dsMap["username"] = ds.User
-				dsMap["password"] = ds.Password
-				dsMap["database"] = ds.Database
+				dsMap["password"] = ds.DecryptedPassword()
 				dsMap["url"] = url
 				dsMap["url"] = url
 			}
 			}
 		}
 		}

+ 3 - 3
pkg/api/pluginproxy/ds_proxy.go

@@ -146,21 +146,21 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
 		if proxy.ds.Type == m.DS_INFLUXDB_08 {
 		if proxy.ds.Type == m.DS_INFLUXDB_08 {
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
 			reqQueryVals.Add("u", proxy.ds.User)
 			reqQueryVals.Add("u", proxy.ds.User)
-			reqQueryVals.Add("p", proxy.ds.Password)
+			reqQueryVals.Add("p", proxy.ds.DecryptedPassword())
 			req.URL.RawQuery = reqQueryVals.Encode()
 			req.URL.RawQuery = reqQueryVals.Encode()
 		} else if proxy.ds.Type == m.DS_INFLUXDB {
 		} else if proxy.ds.Type == m.DS_INFLUXDB {
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
 			req.URL.RawQuery = reqQueryVals.Encode()
 			req.URL.RawQuery = reqQueryVals.Encode()
 			if !proxy.ds.BasicAuth {
 			if !proxy.ds.BasicAuth {
 				req.Header.Del("Authorization")
 				req.Header.Del("Authorization")
-				req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.Password))
+				req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.DecryptedPassword()))
 			}
 			}
 		} else {
 		} else {
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
 		}
 		}
 		if proxy.ds.BasicAuth {
 		if proxy.ds.BasicAuth {
 			req.Header.Del("Authorization")
 			req.Header.Del("Authorization")
-			req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.BasicAuthPassword))
+			req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.DecryptedBasicAuthPassword()))
 		}
 		}
 
 
 		// Lookup and use custom headers
 		// Lookup and use custom headers

+ 108 - 6
pkg/api/pluginproxy/ds_proxy_test.go

@@ -3,6 +3,7 @@ package pluginproxy
 import (
 import (
 	"bytes"
 	"bytes"
 	"fmt"
 	"fmt"
+	"github.com/grafana/grafana/pkg/components/securejsondata"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
@@ -274,12 +275,6 @@ func TestDSRouteRule(t *testing.T) {
 			Convey("Should add db to url", func() {
 			Convey("Should add db to url", func() {
 				So(req.URL.Path, ShouldEqual, "/db/site/")
 				So(req.URL.Path, ShouldEqual, "/db/site/")
 			})
 			})
-
-			Convey("Should add username and password", func() {
-				queryVals := req.URL.Query()
-				So(queryVals["u"][0], ShouldEqual, "user")
-				So(queryVals["p"][0], ShouldEqual, "password")
-			})
 		})
 		})
 
 
 		Convey("When proxying a data source with no keepCookies specified", func() {
 		Convey("When proxying a data source with no keepCookies specified", func() {
@@ -481,6 +476,26 @@ func TestDSRouteRule(t *testing.T) {
 				So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
 				So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
 			})
 			})
 		})
 		})
+
+		Convey("When proxying data source proxy should handle authentication", func() {
+			tests := []*Test{
+				createAuthTest(m.DS_INFLUXDB_08, AUTHTYPE_PASSWORD, AUTHCHECK_QUERY, false),
+				createAuthTest(m.DS_INFLUXDB_08, AUTHTYPE_PASSWORD, AUTHCHECK_QUERY, true),
+				createAuthTest(m.DS_INFLUXDB, AUTHTYPE_PASSWORD, AUTHCHECK_HEADER, true),
+				createAuthTest(m.DS_INFLUXDB, AUTHTYPE_PASSWORD, AUTHCHECK_HEADER, false),
+				createAuthTest(m.DS_INFLUXDB, AUTHTYPE_BASIC, AUTHCHECK_HEADER, true),
+				createAuthTest(m.DS_INFLUXDB, AUTHTYPE_BASIC, AUTHCHECK_HEADER, false),
+
+				// These two should be enough for any other datasource at the moment. Proxy has special handling
+				// only for Influx, others have the same path and only BasicAuth. Non BasicAuth datasources
+				// do not go through proxy but through TSDB API which is not tested here.
+				createAuthTest(m.DS_ES, AUTHTYPE_BASIC, AUTHCHECK_HEADER, false),
+				createAuthTest(m.DS_ES, AUTHTYPE_BASIC, AUTHCHECK_HEADER, true),
+			}
+			for _, test := range tests {
+				runDatasourceAuthTest(test)
+			}
+		})
 	})
 	})
 }
 }
 
 
@@ -524,3 +539,90 @@ func newFakeHTTPClient(fakeBody []byte) httpClient {
 		fakeBody: fakeBody,
 		fakeBody: fakeBody,
 	}
 	}
 }
 }
+
+type Test struct {
+	datasource *m.DataSource
+	checkReq   func(req *http.Request)
+}
+
+const (
+	AUTHTYPE_PASSWORD = "password"
+	AUTHTYPE_BASIC    = "basic"
+)
+
+const (
+	AUTHCHECK_QUERY  = "query"
+	AUTHCHECK_HEADER = "header"
+)
+
+func createAuthTest(dsType string, authType string, authCheck string, useSecureJsonData bool) *Test {
+	// Basic user:password
+	base64AthHeader := "Basic dXNlcjpwYXNzd29yZA=="
+
+	test := &Test{
+		datasource: &m.DataSource{
+			Type:     dsType,
+			JsonData: simplejson.New(),
+		},
+	}
+	var message string
+	if authType == AUTHTYPE_PASSWORD {
+		message = fmt.Sprintf("%v should add username and password", dsType)
+		test.datasource.User = "user"
+		if useSecureJsonData {
+			test.datasource.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
+				"password": "password",
+			})
+		} else {
+			test.datasource.Password = "password"
+		}
+	} else {
+		message = fmt.Sprintf("%v should add basic auth username and password", dsType)
+		test.datasource.BasicAuth = true
+		test.datasource.BasicAuthUser = "user"
+		if useSecureJsonData {
+			test.datasource.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
+				"basicAuthPassword": "password",
+			})
+		} else {
+			test.datasource.BasicAuthPassword = "password"
+		}
+	}
+
+	if useSecureJsonData {
+		message += " from securejsondata"
+	}
+
+	if authCheck == AUTHCHECK_QUERY {
+		message += " to query params"
+		test.checkReq = func(req *http.Request) {
+			Convey(message, func() {
+				queryVals := req.URL.Query()
+				So(queryVals["u"][0], ShouldEqual, "user")
+				So(queryVals["p"][0], ShouldEqual, "password")
+			})
+		}
+	} else {
+		message += " to auth header"
+		test.checkReq = func(req *http.Request) {
+			Convey(message, func() {
+				So(req.Header.Get("Authorization"), ShouldEqual, base64AthHeader)
+			})
+		}
+	}
+
+	return test
+}
+
+func runDatasourceAuthTest(test *Test) {
+	plugin := &plugins.DataSourcePlugin{}
+	ctx := &m.ReqContext{}
+	proxy := NewDataSourceProxy(test.datasource, plugin, ctx, "", &setting.Cfg{})
+
+	req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+	So(err, ShouldBeNil)
+
+	proxy.getDirector()(req)
+
+	test.checkReq(req)
+}

+ 18 - 0
pkg/components/securejsondata/securejsondata.go

@@ -6,8 +6,25 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
+// SecureJsonData is used to store encrypted data (for example in data_source table). Only values are separately
+// encrypted.
 type SecureJsonData map[string][]byte
 type SecureJsonData map[string][]byte
 
 
+// DecryptedValue returns single decrypted value from SecureJsonData. Similar to normal map access second return value
+// is true if the key exists and false if not.
+func (s SecureJsonData) DecryptedValue(key string) (string, bool) {
+	if value, ok := s[key]; ok {
+		decryptedData, err := util.Decrypt(value, setting.SecretKey)
+		if err != nil {
+			log.Fatal(4, err.Error())
+		}
+		return string(decryptedData), true
+	}
+	return "", false
+}
+
+// Decrypt returns map of the same type but where the all the values are decrypted. Opposite of what
+// GetEncryptedJsonData is doing.
 func (s SecureJsonData) Decrypt() map[string]string {
 func (s SecureJsonData) Decrypt() map[string]string {
 	decrypted := make(map[string]string)
 	decrypted := make(map[string]string)
 	for key, data := range s {
 	for key, data := range s {
@@ -21,6 +38,7 @@ func (s SecureJsonData) Decrypt() map[string]string {
 	return decrypted
 	return decrypted
 }
 }
 
 
+// GetEncryptedJsonData returns map where all keys are encrypted.
 func GetEncryptedJsonData(sjd map[string]string) SecureJsonData {
 func GetEncryptedJsonData(sjd map[string]string) SecureJsonData {
 	encrypted := make(SecureJsonData)
 	encrypted := make(SecureJsonData)
 	for key, data := range sjd {
 	for key, data := range sjd {

+ 20 - 0
pkg/models/datasource.go

@@ -61,6 +61,26 @@ type DataSource struct {
 	Updated time.Time
 	Updated time.Time
 }
 }
 
 
+// DecryptedBasicAuthPassword returns data source basic auth password in plain text. It uses either deprecated
+// basic_auth_password field or encrypted secure_json_data[basicAuthPassword] variable.
+func (ds *DataSource) DecryptedBasicAuthPassword() string {
+	return ds.decryptedValue("basicAuthPassword", ds.BasicAuthPassword)
+}
+
+// DecryptedPassword returns data source password in plain text. It uses either deprecated password field
+// or encrypted secure_json_data[password] variable.
+func (ds *DataSource) DecryptedPassword() string {
+	return ds.decryptedValue("password", ds.Password)
+}
+
+// decryptedValue returns decrypted value from secureJsonData
+func (ds *DataSource) decryptedValue(field string, fallback string) string {
+	if value, ok := ds.SecureJsonData.DecryptedValue(field); ok {
+		return value
+	}
+	return fallback
+}
+
 var knownDatasourcePlugins = map[string]bool{
 var knownDatasourcePlugins = map[string]bool{
 	DS_ES:                                 true,
 	DS_ES:                                 true,
 	DS_GRAPHITE:                           true,
 	DS_GRAPHITE:                           true,

+ 2 - 2
pkg/services/provisioning/datasources/config_reader.go

@@ -62,8 +62,8 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D
 	}
 	}
 
 
 	if apiVersion.ApiVersion > 0 {
 	if apiVersion.ApiVersion > 0 {
-		var v1 *DatasourcesAsConfigV1
-		err = yaml.Unmarshal(yamlFile, &v1)
+		v1 := &DatasourcesAsConfigV1{log: cr.log}
+		err = yaml.Unmarshal(yamlFile, v1)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}

+ 9 - 1
pkg/services/provisioning/datasources/types.go

@@ -1,9 +1,10 @@
 package datasources
 package datasources
 
 
 import (
 import (
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
 )
 )
-import "github.com/grafana/grafana/pkg/components/simplejson"
 
 
 type ConfigVersion struct {
 type ConfigVersion struct {
 	ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
 	ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
@@ -51,6 +52,7 @@ type DatasourcesAsConfigV0 struct {
 
 
 type DatasourcesAsConfigV1 struct {
 type DatasourcesAsConfigV1 struct {
 	ConfigVersion
 	ConfigVersion
+	log log.Logger
 
 
 	Datasources       []*DataSourceFromConfigV1   `json:"datasources" yaml:"datasources"`
 	Datasources       []*DataSourceFromConfigV1   `json:"datasources" yaml:"datasources"`
 	DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
 	DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
@@ -135,6 +137,12 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D
 			Editable:          ds.Editable,
 			Editable:          ds.Editable,
 			Version:           ds.Version,
 			Version:           ds.Version,
 		})
 		})
+		if ds.Password != "" {
+			cfg.log.Warn("[Deprecated] the use of password field is deprecated. Please use secureJsonData.password", "datasource name", ds.Name)
+		}
+		if ds.BasicAuthPassword != "" {
+			cfg.log.Warn("[Deprecated] the use of basicAuthPassword field is deprecated. Please use secureJsonData.basicAuthPassword", "datasource name", ds.Name)
+		}
 	}
 	}
 
 
 	for _, ds := range cfg.DeleteDatasources {
 	for _, ds := range cfg.DeleteDatasources {

+ 4 - 0
pkg/services/sqlstore/datasource.go

@@ -178,6 +178,10 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 		sess.UseBool("basic_auth")
 		sess.UseBool("basic_auth")
 		sess.UseBool("with_credentials")
 		sess.UseBool("with_credentials")
 		sess.UseBool("read_only")
 		sess.UseBool("read_only")
+		// Make sure password are zeroed out if empty. We do this as we want to migrate passwords from
+		// plain text fields to SecureJsonData.
+		sess.MustCols("password")
+		sess.MustCols("basic_auth_password")
 
 
 		var updateSession *xorm.Session
 		var updateSession *xorm.Session
 		if cmd.Version != 0 {
 		if cmd.Version != 0 {

+ 2 - 2
pkg/tsdb/elasticsearch/client/client.go

@@ -172,12 +172,12 @@ func (c *baseClientImpl) executeRequest(method, uriPath string, body []byte) (*h
 
 
 	if c.ds.BasicAuth {
 	if c.ds.BasicAuth {
 		clientLog.Debug("Request configured to use basic authentication")
 		clientLog.Debug("Request configured to use basic authentication")
-		req.SetBasicAuth(c.ds.BasicAuthUser, c.ds.BasicAuthPassword)
+		req.SetBasicAuth(c.ds.BasicAuthUser, c.ds.DecryptedBasicAuthPassword())
 	}
 	}
 
 
 	if !c.ds.BasicAuth && c.ds.User != "" {
 	if !c.ds.BasicAuth && c.ds.User != "" {
 		clientLog.Debug("Request configured to use basic authentication")
 		clientLog.Debug("Request configured to use basic authentication")
-		req.SetBasicAuth(c.ds.User, c.ds.Password)
+		req.SetBasicAuth(c.ds.User, c.ds.DecryptedPassword())
 	}
 	}
 
 
 	httpClient, err := newDatasourceHttpClient(c.ds)
 	httpClient, err := newDatasourceHttpClient(c.ds)

+ 1 - 1
pkg/tsdb/graphite/graphite.go

@@ -149,7 +149,7 @@ func (e *GraphiteExecutor) createRequest(dsInfo *models.DataSource, data url.Val
 
 
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	if dsInfo.BasicAuth {
 	if dsInfo.BasicAuth {
-		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword)
+		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
 	}
 	}
 
 
 	return req, err
 	return req, err

+ 2 - 2
pkg/tsdb/influxdb/influxdb.go

@@ -125,11 +125,11 @@ func (e *InfluxDBExecutor) createRequest(dsInfo *models.DataSource, query string
 	req.Header.Set("User-Agent", "Grafana")
 	req.Header.Set("User-Agent", "Grafana")
 
 
 	if dsInfo.BasicAuth {
 	if dsInfo.BasicAuth {
-		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword)
+		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
 	}
 	}
 
 
 	if !dsInfo.BasicAuth && dsInfo.User != "" {
 	if !dsInfo.BasicAuth && dsInfo.User != "" {
-		req.SetBasicAuth(dsInfo.User, dsInfo.Password)
+		req.SetBasicAuth(dsInfo.User, dsInfo.DecryptedPassword())
 	}
 	}
 
 
 	glog.Debug("Influxdb request", "url", req.URL.String())
 	glog.Debug("Influxdb request", "url", req.URL.String())

+ 1 - 9
pkg/tsdb/mssql/mssql.go

@@ -44,14 +44,6 @@ func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
 }
 }
 
 
 func generateConnectionString(datasource *models.DataSource) (string, error) {
 func generateConnectionString(datasource *models.DataSource) (string, error) {
-	password := ""
-	for key, value := range datasource.SecureJsonData.Decrypt() {
-		if key == "password" {
-			password = value
-			break
-		}
-	}
-
 	server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
 	server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
 
 
 	encrypt := datasource.JsonData.Get("encrypt").MustString("false")
 	encrypt := datasource.JsonData.Get("encrypt").MustString("false")
@@ -60,7 +52,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) {
 		port,
 		port,
 		datasource.Database,
 		datasource.Database,
 		datasource.User,
 		datasource.User,
-		password,
+		datasource.DecryptedPassword(),
 	)
 	)
 	if encrypt != "false" {
 	if encrypt != "false" {
 		connStr += fmt.Sprintf("encrypt=%s;", encrypt)
 		connStr += fmt.Sprintf("encrypt=%s;", encrypt)

+ 1 - 1
pkg/tsdb/mysql/mysql.go

@@ -28,7 +28,7 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
 	}
 	}
 	cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
 	cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
 		datasource.User,
 		datasource.User,
-		datasource.Password,
+		datasource.DecryptedPassword(),
 		protocol,
 		protocol,
 		datasource.Url,
 		datasource.Url,
 		datasource.Database,
 		datasource.Database,

+ 1 - 1
pkg/tsdb/opentsdb/opentsdb.go

@@ -96,7 +96,7 @@ func (e *OpenTsdbExecutor) createRequest(dsInfo *models.DataSource, data OpenTsd
 
 
 	req.Header.Set("Content-Type", "application/json")
 	req.Header.Set("Content-Type", "application/json")
 	if dsInfo.BasicAuth {
 	if dsInfo.BasicAuth {
-		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword)
+		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
 	}
 	}
 
 
 	return req, err
 	return req, err

+ 1 - 9
pkg/tsdb/postgres/postgres.go

@@ -41,18 +41,10 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp
 }
 }
 
 
 func generateConnectionString(datasource *models.DataSource) string {
 func generateConnectionString(datasource *models.DataSource) string {
-	password := ""
-	for key, value := range datasource.SecureJsonData.Decrypt() {
-		if key == "password" {
-			password = value
-			break
-		}
-	}
-
 	sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
 	sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
 	u := &url.URL{
 	u := &url.URL{
 		Scheme: "postgres",
 		Scheme: "postgres",
-		User:   url.UserPassword(datasource.User, password),
+		User:   url.UserPassword(datasource.User, datasource.DecryptedPassword()),
 		Host:   datasource.Url, Path: datasource.Database,
 		Host:   datasource.Url, Path: datasource.Database,
 		RawQuery: "sslmode=" + url.QueryEscape(sslmode),
 		RawQuery: "sslmode=" + url.QueryEscape(sslmode),
 	}
 	}

+ 1 - 1
pkg/tsdb/prometheus/prometheus.go

@@ -70,7 +70,7 @@ func (e *PrometheusExecutor) getClient(dsInfo *models.DataSource) (apiv1.API, er
 		cfg.RoundTripper = basicAuthTransport{
 		cfg.RoundTripper = basicAuthTransport{
 			Transport: e.Transport,
 			Transport: e.Transport,
 			username:  dsInfo.BasicAuthUser,
 			username:  dsInfo.BasicAuthUser,
-			password:  dsInfo.BasicAuthPassword,
+			password:  dsInfo.DecryptedBasicAuthPassword(),
 		}
 		}
 	}
 	}
 
 

+ 1 - 0
public/app/core/angular_wrappers.ts

@@ -63,6 +63,7 @@ export function registerAngularDirectives() {
     'value',
     'value',
     'isConfigured',
     'isConfigured',
     'inputWidth',
     'inputWidth',
+    'labelWidth',
     ['onReset', { watchDepth: 'reference', wrapApply: true }],
     ['onReset', { watchDepth: 'reference', wrapApply: true }],
     ['onChange', { watchDepth: 'reference', wrapApply: true }],
     ['onChange', { watchDepth: 'reference', wrapApply: true }],
   ]);
   ]);

+ 8 - 2
public/app/features/datasources/partials/http_settings.html

@@ -99,8 +99,14 @@
 			<input class="gf-form-input max-width-21" type="text"  ng-model='current.basicAuthUser' placeholder="user" required></input>
 			<input class="gf-form-input max-width-21" type="text"  ng-model='current.basicAuthUser' placeholder="user" required></input>
 		</div>
 		</div>
 		<div class="gf-form">
 		<div class="gf-form">
-			<span class="gf-form-label width-10">Password</span>
-			<input class="gf-form-input max-width-21" type="password" ng-model='current.basicAuthPassword' placeholder="password" required></input>
+      <secret-form-field
+        isConfigured="current.basicAuthPassword || current.secureJsonFields.basicAuthPassword"
+        value="current.secureJsonData.basicAuthPassword || ''"
+        on-reset="onBasicAuthPasswordReset"
+        on-change="onBasicAuthPasswordChange"
+        inputWidth="18"
+        labelWidth="10"
+      />
 		</div>
 		</div>
 	</div>
 	</div>
 
 

+ 4 - 0
public/app/features/datasources/settings/HttpSettingsCtrl.ts

@@ -1,4 +1,5 @@
 import { coreModule } from 'app/core/core';
 import { coreModule } from 'app/core/core';
+import { createChangeHandler, createResetHandler, PasswordFieldEnum } from '../utils/passwordHandlers';
 
 
 coreModule.directive('datasourceHttpSettings', () => {
 coreModule.directive('datasourceHttpSettings', () => {
   return {
   return {
@@ -20,6 +21,9 @@ coreModule.directive('datasourceHttpSettings', () => {
         $scope.getSuggestUrls = () => {
         $scope.getSuggestUrls = () => {
           return [$scope.suggestUrl];
           return [$scope.suggestUrl];
         };
         };
+
+        $scope.onBasicAuthPasswordReset = createResetHandler($scope, PasswordFieldEnum.BasicAuthPassword);
+        $scope.onBasicAuthPasswordChange = createChangeHandler($scope, PasswordFieldEnum.BasicAuthPassword);
       },
       },
     },
     },
   };
   };

+ 35 - 0
public/app/features/datasources/utils/passwordHandlers.test.ts

@@ -0,0 +1,35 @@
+import { createResetHandler, PasswordFieldEnum, Ctrl } from './passwordHandlers';
+
+describe('createResetHandler', () => {
+  Object.keys(PasswordFieldEnum).forEach(fieldKey => {
+    const field = PasswordFieldEnum[fieldKey];
+
+    it(`should reset existing ${field} field`, () => {
+      const event: any = {
+        preventDefault: () => {},
+      };
+      const ctrl: Ctrl = {
+        current: {
+          [field]: 'set',
+          secureJsonData: {
+            [field]: 'set',
+          },
+          secureJsonFields: {},
+        },
+      };
+
+      createResetHandler(ctrl, field)(event);
+      expect(ctrl).toEqual({
+        current: {
+          [field]: null,
+          secureJsonData: {
+            [field]: '',
+          },
+          secureJsonFields: {
+            [field]: false,
+          },
+        },
+      });
+    });
+  });
+});

+ 39 - 0
public/app/features/datasources/utils/passwordHandlers.ts

@@ -0,0 +1,39 @@
+/**
+ * Set of handlers for secure password field in Angular components. They handle backward compatibility with
+ * passwords stored in plain text fields.
+ */
+
+import { SyntheticEvent } from 'react';
+
+export enum PasswordFieldEnum {
+  Password = 'password',
+  BasicAuthPassword = 'basicAuthPassword',
+}
+
+/**
+ * Basic shape for settings controllers in at the moment mostly angular datasource plugins.
+ */
+export type Ctrl = {
+  current: {
+    secureJsonFields: {};
+    secureJsonData?: {};
+  };
+};
+
+export const createResetHandler = (ctrl: Ctrl, field: PasswordFieldEnum) => (
+  event: SyntheticEvent<HTMLInputElement>
+) => {
+  event.preventDefault();
+  // Reset also normal plain text password to remove it and only save it in secureJsonData.
+  ctrl.current[field] = null;
+  ctrl.current.secureJsonFields[field] = false;
+  ctrl.current.secureJsonData = ctrl.current.secureJsonData || {};
+  ctrl.current.secureJsonData[field] = '';
+};
+
+export const createChangeHandler = (ctrl: any, field: PasswordFieldEnum) => (
+  event: SyntheticEvent<HTMLInputElement>
+) => {
+  ctrl.current.secureJsonData = ctrl.current.secureJsonData || {};
+  ctrl.current.secureJsonData[field] = event.currentTarget.value;
+};

+ 13 - 0
public/app/plugins/datasource/influxdb/module.ts

@@ -1,8 +1,21 @@
 import InfluxDatasource from './datasource';
 import InfluxDatasource from './datasource';
 import { InfluxQueryCtrl } from './query_ctrl';
 import { InfluxQueryCtrl } from './query_ctrl';
+import {
+  createChangeHandler,
+  createResetHandler,
+  PasswordFieldEnum,
+} from '../../../features/datasources/utils/passwordHandlers';
 
 
 class InfluxConfigCtrl {
 class InfluxConfigCtrl {
   static templateUrl = 'partials/config.html';
   static templateUrl = 'partials/config.html';
+  current: any;
+  onPasswordReset: ReturnType<typeof createResetHandler>;
+  onPasswordChange: ReturnType<typeof createChangeHandler>;
+
+  constructor() {
+    this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
+    this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
+  }
 }
 }
 
 
 class InfluxAnnotationsQueryCtrl {
 class InfluxAnnotationsQueryCtrl {

+ 8 - 3
public/app/plugins/datasource/influxdb/partials/config.html

@@ -16,9 +16,14 @@
 			<span class="gf-form-label width-10">User</span>
 			<span class="gf-form-label width-10">User</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder=""></input>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder=""></input>
 		</div>
 		</div>
-		<div class="gf-form max-width-15">
-			<span class="gf-form-label width-10">Password</span>
-			<input type="password" class="gf-form-input" ng-model='ctrl.current.password' placeholder=""></input>
+		<div class="gf-form">
+      <secret-form-field
+        isConfigured="ctrl.current.password || ctrl.current.secureJsonFields.password"
+        value="ctrl.current.secureJsonData.password || ''"
+        on-reset="ctrl.onPasswordReset"
+        on-change="ctrl.onPasswordChange"
+        inputWidth="9"
+      />
 		</div>
 		</div>
 	</div>
 	</div>
 </div>
 </div>

+ 9 - 13
public/app/plugins/datasource/mssql/config_ctrl.ts

@@ -1,24 +1,20 @@
-import { SyntheticEvent } from 'react';
+import {
+  createChangeHandler,
+  createResetHandler,
+  PasswordFieldEnum,
+} from '../../../features/datasources/utils/passwordHandlers';
 
 
 export class MssqlConfigCtrl {
 export class MssqlConfigCtrl {
   static templateUrl = 'partials/config.html';
   static templateUrl = 'partials/config.html';
 
 
   current: any;
   current: any;
+  onPasswordReset: ReturnType<typeof createResetHandler>;
+  onPasswordChange: ReturnType<typeof createChangeHandler>;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor($scope) {
   constructor($scope) {
     this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false';
     this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false';
+    this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
+    this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
   }
   }
-
-  onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
-    event.preventDefault();
-    this.current.secureJsonFields.password = false;
-    this.current.secureJsonData = this.current.secureJsonData || {};
-    this.current.secureJsonData.password = '';
-  };
-
-  onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
-    this.current.secureJsonData = this.current.secureJsonData || {};
-    this.current.secureJsonData.password = event.currentTarget.value;
-  };
 }
 }

+ 13 - 0
public/app/plugins/datasource/mysql/module.ts

@@ -1,8 +1,21 @@
 import { MysqlDatasource } from './datasource';
 import { MysqlDatasource } from './datasource';
 import { MysqlQueryCtrl } from './query_ctrl';
 import { MysqlQueryCtrl } from './query_ctrl';
+import {
+  createChangeHandler,
+  createResetHandler,
+  PasswordFieldEnum,
+} from '../../../features/datasources/utils/passwordHandlers';
 
 
 class MysqlConfigCtrl {
 class MysqlConfigCtrl {
   static templateUrl = 'partials/config.html';
   static templateUrl = 'partials/config.html';
+  current: any;
+  onPasswordReset: ReturnType<typeof createResetHandler>;
+  onPasswordChange: ReturnType<typeof createChangeHandler>;
+
+  constructor() {
+    this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
+    this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
+  }
 }
 }
 
 
 const defaultQuery = `SELECT
 const defaultQuery = `SELECT

+ 8 - 3
public/app/plugins/datasource/mysql/partials/config.html

@@ -16,9 +16,14 @@
 			<span class="gf-form-label width-7">User</span>
 			<span class="gf-form-label width-7">User</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
 		</div>
 		</div>
-		<div class="gf-form max-width-15">
-			<span class="gf-form-label width-7">Password</span>
-			<input type="password" class="gf-form-input" ng-model='ctrl.current.password' placeholder="password"></input>
+		<div class="gf-form">
+      <secret-form-field
+        isConfigured="ctrl.current.secureJsonFields.password"
+        value="ctrl.current.secureJsonData.password"
+        on-reset="ctrl.onPasswordReset"
+        on-change="ctrl.onPasswordChange"
+        inputWidth="9"
+      />
 		</div>
 		</div>
 	</div>
 	</div>
 
 

+ 9 - 13
public/app/plugins/datasource/postgres/config_ctrl.ts

@@ -1,5 +1,9 @@
 import _ from 'lodash';
 import _ from 'lodash';
-import { SyntheticEvent } from 'react';
+import {
+  createChangeHandler,
+  createResetHandler,
+  PasswordFieldEnum,
+} from '../../../features/datasources/utils/passwordHandlers';
 
 
 export class PostgresConfigCtrl {
 export class PostgresConfigCtrl {
   static templateUrl = 'partials/config.html';
   static templateUrl = 'partials/config.html';
@@ -7,6 +11,8 @@ export class PostgresConfigCtrl {
   current: any;
   current: any;
   datasourceSrv: any;
   datasourceSrv: any;
   showTimescaleDBHelp: boolean;
   showTimescaleDBHelp: boolean;
+  onPasswordReset: ReturnType<typeof createResetHandler>;
+  onPasswordChange: ReturnType<typeof createChangeHandler>;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor($scope, datasourceSrv) {
   constructor($scope, datasourceSrv) {
@@ -15,6 +21,8 @@ export class PostgresConfigCtrl {
     this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903;
     this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903;
     this.showTimescaleDBHelp = false;
     this.showTimescaleDBHelp = false;
     this.autoDetectFeatures();
     this.autoDetectFeatures();
+    this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
+    this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
   }
   }
 
 
   autoDetectFeatures() {
   autoDetectFeatures() {
@@ -53,18 +61,6 @@ export class PostgresConfigCtrl {
     this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
     this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
   }
   }
 
 
-  onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
-    event.preventDefault();
-    this.current.secureJsonFields.password = false;
-    this.current.secureJsonData = this.current.secureJsonData || {};
-    this.current.secureJsonData.password = '';
-  };
-
-  onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
-    this.current.secureJsonData = this.current.secureJsonData || {};
-    this.current.secureJsonData.password = event.currentTarget.value;
-  };
-
   // the value portion is derived from postgres server_version_num/100
   // the value portion is derived from postgres server_version_num/100
   postgresVersions = [
   postgresVersions = [
     { name: '9.3', value: 903 },
     { name: '9.3', value: 903 },