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:
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user