mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
844ec82eb0
commit
66f6e16916
@ -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
|
||||
|
@ -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 |
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,7 @@ export function registerAngularDirectives() {
|
||||
'value',
|
||||
'isConfigured',
|
||||
'inputWidth',
|
||||
'labelWidth',
|
||||
['onReset', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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
public/app/features/datasources/utils/passwordHandlers.ts
Normal file
39
public/app/features/datasources/utils/passwordHandlers.ts
Normal file
@ -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;
|
||||
};
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 },
|
||||
|
Loading…
Reference in New Issue
Block a user