Browse Source

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 years ago
parent
commit
66f6e16916
30 changed files with 352 additions and 85 deletions
  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
     database: site
     user: grafana
-    password: grafana
     url: http://localhost:8086
     jsonData:
       timeInterval: "15s"
+    secureJsonData:
+      password: grafana
 
   - name: gdev-opentsdb
     type: opentsdb
@@ -110,14 +111,16 @@ datasources:
     url: localhost:3306
     database: grafana
     user: grafana
-    password: password
+    secureJsonData:
+      password: password
 
   - name: gdev-mysql-ds-tests
     type: mysql
     url: localhost:3306
     database: grafana_ds_tests
     user: grafana
-    password: password
+    secureJsonData:
+      password: password
 
   - name: gdev-mssql
     type: mssql

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

@@ -107,7 +107,7 @@ datasources:
   orgId: 1
   # <string> url
   url: http://localhost:8080
-  # <string> database password, if used
+  # <string> Deprecated, use secureJsonData.password
   password:
   # <string> database user, if used
   user:
@@ -117,7 +117,7 @@ datasources:
   basicAuth:
   # <string> basic auth username
   basicAuthUser:
-  # <string> basic auth password
+  # <string> Deprecated, use secureJsonData.basicAuthPassword
   basicAuthPassword:
   # <bool> enable/disable with credentials headers
   withCredentials:
@@ -133,6 +133,10 @@ datasources:
     tlsCACert: "..."
     tlsClientCert: "..."
     tlsClientKey: "..."
+    # <string> database password, if used
+    password:
+    # <string> basic auth password
+    basicAuthPassword:
   version: 1
   # <bool> allow users to edit datasources from the UI.
   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 |
 | tlsClientCert | string | *All* |TLS Client cert 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 |
 | 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.
 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.
 

+ 5 - 5
pkg/api/frontendsettings.go

@@ -1,6 +1,7 @@
 package api
 
 import (
+	"github.com/grafana/grafana/pkg/util"
 	"strconv"
 
 	"github.com/grafana/grafana/pkg/bus"
@@ -8,9 +9,9 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"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) {
 	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.BasicAuth {
-				dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
+				dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.DecryptedBasicAuthPassword())
 			}
 			if 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 {
 				dsMap["username"] = ds.User
-				dsMap["password"] = ds.Password
+				dsMap["password"] = ds.DecryptedPassword()
 				dsMap["url"] = url + "/db/" + ds.Database
 			}
 
 			if ds.Type == m.DS_INFLUXDB {
 				dsMap["username"] = ds.User
-				dsMap["password"] = ds.Password
-				dsMap["database"] = ds.Database
+				dsMap["password"] = ds.DecryptedPassword()
 				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 {
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
 			reqQueryVals.Add("u", proxy.ds.User)
-			reqQueryVals.Add("p", proxy.ds.Password)
+			reqQueryVals.Add("p", proxy.ds.DecryptedPassword())
 			req.URL.RawQuery = reqQueryVals.Encode()
 		} else if proxy.ds.Type == m.DS_INFLUXDB {
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
 			req.URL.RawQuery = reqQueryVals.Encode()
 			if !proxy.ds.BasicAuth {
 				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 {
 			req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
 		}
 		if proxy.ds.BasicAuth {
 			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

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

@@ -3,6 +3,7 @@ package pluginproxy
 import (
 	"bytes"
 	"fmt"
+	"github.com/grafana/grafana/pkg/components/securejsondata"
 	"io/ioutil"
 	"net/http"
 	"net/url"
@@ -274,12 +275,6 @@ func TestDSRouteRule(t *testing.T) {
 			Convey("Should add db to url", func() {
 				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() {
@@ -481,6 +476,26 @@ func TestDSRouteRule(t *testing.T) {
 				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,
 	}
 }
+
+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"
 )
 
+// SecureJsonData is used to store encrypted data (for example in data_source table). Only values are separately
+// encrypted.
 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 {
 	decrypted := make(map[string]string)
 	for key, data := range s {
@@ -21,6 +38,7 @@ func (s SecureJsonData) Decrypt() map[string]string {
 	return decrypted
 }
 
+// GetEncryptedJsonData returns map where all keys are encrypted.
 func GetEncryptedJsonData(sjd map[string]string) SecureJsonData {
 	encrypted := make(SecureJsonData)
 	for key, data := range sjd {

+ 20 - 0
pkg/models/datasource.go

@@ -61,6 +61,26 @@ type DataSource struct {
 	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{
 	DS_ES:                                 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 {
-		var v1 *DatasourcesAsConfigV1
-		err = yaml.Unmarshal(yamlFile, &v1)
+		v1 := &DatasourcesAsConfigV1{log: cr.log}
+		err = yaml.Unmarshal(yamlFile, v1)
 		if err != nil {
 			return nil, err
 		}

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

@@ -1,9 +1,10 @@
 package datasources
 
 import (
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 )
-import "github.com/grafana/grafana/pkg/components/simplejson"
 
 type ConfigVersion struct {
 	ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
@@ -51,6 +52,7 @@ type DatasourcesAsConfigV0 struct {
 
 type DatasourcesAsConfigV1 struct {
 	ConfigVersion
+	log log.Logger
 
 	Datasources       []*DataSourceFromConfigV1   `json:"datasources" yaml:"datasources"`
 	DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
@@ -135,6 +137,12 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D
 			Editable:          ds.Editable,
 			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 {

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

@@ -178,6 +178,10 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 		sess.UseBool("basic_auth")
 		sess.UseBool("with_credentials")
 		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
 		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 {
 		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 != "" {
 		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)

+ 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")
 	if dsInfo.BasicAuth {
-		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword)
+		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
 	}
 
 	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")
 
 	if dsInfo.BasicAuth {
-		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword)
+		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
 	}
 
 	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())

+ 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) {
-	password := ""
-	for key, value := range datasource.SecureJsonData.Decrypt() {
-		if key == "password" {
-			password = value
-			break
-		}
-	}
-
 	server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
 
 	encrypt := datasource.JsonData.Get("encrypt").MustString("false")
@@ -60,7 +52,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) {
 		port,
 		datasource.Database,
 		datasource.User,
-		password,
+		datasource.DecryptedPassword(),
 	)
 	if encrypt != "false" {
 		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",
 		datasource.User,
-		datasource.Password,
+		datasource.DecryptedPassword(),
 		protocol,
 		datasource.Url,
 		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")
 	if dsInfo.BasicAuth {
-		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword)
+		req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
 	}
 
 	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 {
-	password := ""
-	for key, value := range datasource.SecureJsonData.Decrypt() {
-		if key == "password" {
-			password = value
-			break
-		}
-	}
-
 	sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
 	u := &url.URL{
 		Scheme: "postgres",
-		User:   url.UserPassword(datasource.User, password),
+		User:   url.UserPassword(datasource.User, datasource.DecryptedPassword()),
 		Host:   datasource.Url, Path: datasource.Database,
 		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{
 			Transport: e.Transport,
 			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',
     'isConfigured',
     'inputWidth',
+    'labelWidth',
     ['onReset', { 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>
 		</div>
 		<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>
 

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

@@ -1,4 +1,5 @@
 import { coreModule } from 'app/core/core';
+import { createChangeHandler, createResetHandler, PasswordFieldEnum } from '../utils/passwordHandlers';
 
 coreModule.directive('datasourceHttpSettings', () => {
   return {
@@ -20,6 +21,9 @@ coreModule.directive('datasourceHttpSettings', () => {
         $scope.getSuggestUrls = () => {
           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 { InfluxQueryCtrl } from './query_ctrl';
+import {
+  createChangeHandler,
+  createResetHandler,
+  PasswordFieldEnum,
+} from '../../../features/datasources/utils/passwordHandlers';
 
 class InfluxConfigCtrl {
   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 {

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

@@ -16,9 +16,14 @@
 			<span class="gf-form-label width-10">User</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder=""></input>
 		</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>

+ 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 {
   static templateUrl = 'partials/config.html';
 
   current: any;
+  onPasswordReset: ReturnType<typeof createResetHandler>;
+  onPasswordChange: ReturnType<typeof createChangeHandler>;
 
   /** @ngInject */
   constructor($scope) {
     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 { MysqlQueryCtrl } from './query_ctrl';
+import {
+  createChangeHandler,
+  createResetHandler,
+  PasswordFieldEnum,
+} from '../../../features/datasources/utils/passwordHandlers';
 
 class MysqlConfigCtrl {
   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

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

@@ -16,9 +16,14 @@
 			<span class="gf-form-label width-7">User</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
 		</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>
 

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

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