diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml index f50fdeeb5c4..d138f6ed325 100644 --- a/devenv/datasources.yaml +++ b/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 diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index d34def09701..b1513940ef2 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -107,7 +107,7 @@ datasources: orgId: 1 # url url: http://localhost:8080 - # database password, if used + # Deprecated, use secureJsonData.password password: # database user, if used user: @@ -117,7 +117,7 @@ datasources: basicAuth: # basic auth username basicAuthUser: - # basic auth password + # Deprecated, use secureJsonData.basicAuthPassword basicAuthPassword: # enable/disable with credentials headers withCredentials: @@ -133,6 +133,10 @@ datasources: tlsCACert: "..." tlsClientCert: "..." tlsClientKey: "..." + # database password, if used + password: + # basic auth password + basicAuthPassword: version: 1 # 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 | diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index 511d6117eb3..74de4ae8873 100644 --- a/docs/sources/installation/upgrading.md +++ b/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. diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 847ed6b9366..1fceca12bfa 100644 --- a/pkg/api/frontendsettings.go +++ b/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 } } diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 5f314bc2421..b86d1834844 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/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 diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 368acb3a642..e9e526fb7c3 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/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) +} diff --git a/pkg/components/securejsondata/securejsondata.go b/pkg/components/securejsondata/securejsondata.go index caf4b369483..4e7f6360173 100644 --- a/pkg/components/securejsondata/securejsondata.go +++ b/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 { diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 22c53dfa0dd..491126e4fc6 100644 --- a/pkg/models/datasource.go +++ b/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, diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 34c1418aa98..334ce879e5a 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/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 } diff --git a/pkg/services/provisioning/datasources/types.go b/pkg/services/provisioning/datasources/types.go index 8e2443a0169..f619fd5f005 100644 --- a/pkg/services/provisioning/datasources/types.go +++ b/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 { diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 071577eb6d6..495e3cee93f 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/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 { diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index f5a200f60ce..22e34a12b65 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/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) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 108e9188329..9a5de205a23 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/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 diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 3502f1ac196..d199211575a 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/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()) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index c740d6cbe77..6503632aea3 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/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) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 0451f8f0dc1..95a9e02598f 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/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, diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index a810d3c7338..d0f61e05233 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/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 diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index ae6b165e731..7840a47fb18 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/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), } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 83bb683fccf..bb343aea26e 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/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(), } } diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 60e292cc24b..4460d104f6b 100644 --- a/public/app/core/angular_wrappers.ts +++ b/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 }], ]); diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/datasources/partials/http_settings.html index 755284bf7a8..3c078f4befe 100644 --- a/public/app/features/datasources/partials/http_settings.html +++ b/public/app/features/datasources/partials/http_settings.html @@ -99,8 +99,14 @@
- Password - +
diff --git a/public/app/features/datasources/settings/HttpSettingsCtrl.ts b/public/app/features/datasources/settings/HttpSettingsCtrl.ts index 5cdebe7b9ab..5b233118057 100644 --- a/public/app/features/datasources/settings/HttpSettingsCtrl.ts +++ b/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); }, }, }; diff --git a/public/app/features/datasources/utils/passwordHandlers.test.ts b/public/app/features/datasources/utils/passwordHandlers.test.ts new file mode 100644 index 00000000000..2dddc4fc130 --- /dev/null +++ b/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, + }, + }, + }); + }); + }); +}); diff --git a/public/app/features/datasources/utils/passwordHandlers.ts b/public/app/features/datasources/utils/passwordHandlers.ts new file mode 100644 index 00000000000..f7bcdc1aec2 --- /dev/null +++ b/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 +) => { + 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 +) => { + ctrl.current.secureJsonData = ctrl.current.secureJsonData || {}; + ctrl.current.secureJsonData[field] = event.currentTarget.value; +}; diff --git a/public/app/plugins/datasource/influxdb/module.ts b/public/app/plugins/datasource/influxdb/module.ts index 5997a7d061b..b25c15101c1 100644 --- a/public/app/plugins/datasource/influxdb/module.ts +++ b/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; + onPasswordChange: ReturnType; + + constructor() { + this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password); + this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password); + } } class InfluxAnnotationsQueryCtrl { diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 0229d01e8c8..01431639690 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -16,9 +16,14 @@ User -
- Password - +
+
diff --git a/public/app/plugins/datasource/mssql/config_ctrl.ts b/public/app/plugins/datasource/mssql/config_ctrl.ts index 4555e6f67d8..27e1d59ba24 100644 --- a/public/app/plugins/datasource/mssql/config_ctrl.ts +++ b/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; + onPasswordChange: ReturnType; /** @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) => { - event.preventDefault(); - this.current.secureJsonFields.password = false; - this.current.secureJsonData = this.current.secureJsonData || {}; - this.current.secureJsonData.password = ''; - }; - - onPasswordChange = (event: SyntheticEvent) => { - this.current.secureJsonData = this.current.secureJsonData || {}; - this.current.secureJsonData.password = event.currentTarget.value; - }; } diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index 2d8caf17af4..f5181d3be82 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/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; + onPasswordChange: ReturnType; + + constructor() { + this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password); + this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password); + } } const defaultQuery = `SELECT diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html index f08d9b3da15..4ec8101a227 100644 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ b/public/app/plugins/datasource/mysql/partials/config.html @@ -16,9 +16,14 @@ User -
- Password - +
+
diff --git a/public/app/plugins/datasource/postgres/config_ctrl.ts b/public/app/plugins/datasource/postgres/config_ctrl.ts index e547f5697a2..1a12deafa46 100644 --- a/public/app/plugins/datasource/postgres/config_ctrl.ts +++ b/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; + onPasswordChange: ReturnType; /** @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) => { - event.preventDefault(); - this.current.secureJsonFields.password = false; - this.current.secureJsonData = this.current.secureJsonData || {}; - this.current.secureJsonData.password = ''; - }; - - onPasswordChange = (event: SyntheticEvent) => { - 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 },