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:
Andrej Ocenas
2019-04-15 11:11:17 +02:00
committed by GitHub
parent 844ec82eb0
commit 66f6e16916
30 changed files with 352 additions and 85 deletions
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+20
View File
@@ -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 {
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(),
}
}