mirror of https://github.com/grafana/grafana
CLI: Add command to migrate all datasources to use encrypted password fields (#17118)
closes: #17107pull/17301/head
parent
b9181df212
commit
151b24b95f
@ -0,0 +1,126 @@ |
||||
package datamigrations |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
|
||||
"github.com/fatih/color" |
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" |
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"github.com/grafana/grafana/pkg/util/errutil" |
||||
) |
||||
|
||||
var ( |
||||
datasourceTypes = []string{ |
||||
"mysql", |
||||
"influxdb", |
||||
"elasticsearch", |
||||
"graphite", |
||||
"prometheus", |
||||
"opentsdb", |
||||
} |
||||
) |
||||
|
||||
// EncryptDatasourcePaswords migrates un-encrypted secrets on datasources
|
||||
// to the secureJson Column.
|
||||
func EncryptDatasourcePaswords(c utils.CommandLine, sqlStore *sqlstore.SqlStore) error { |
||||
return sqlStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error { |
||||
passwordsUpdated, err := migrateColumn(session, "password") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
basicAuthUpdated, err := migrateColumn(session, "basic_auth_password") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
logger.Info("\n") |
||||
if passwordsUpdated > 0 { |
||||
logger.Infof("%s Encrypted password field for %d datasources \n", color.GreenString("✔"), passwordsUpdated) |
||||
} |
||||
|
||||
if basicAuthUpdated > 0 { |
||||
logger.Infof("%s Encrypted basic_auth_password field for %d datasources \n", color.GreenString("✔"), basicAuthUpdated) |
||||
} |
||||
|
||||
if passwordsUpdated == 0 && basicAuthUpdated == 0 { |
||||
logger.Infof("%s All datasources secrets are allready encrypted\n", color.GreenString("✔")) |
||||
} |
||||
|
||||
logger.Info("\n") |
||||
|
||||
logger.Warn("Warning: Datasource provisioning files need to be manually changed to prevent overwriting of " + |
||||
"the data during provisioning. See https://grafana.com/docs/installation/upgrading/#upgrading-to-v6-2 for " + |
||||
"details") |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
func migrateColumn(session *sqlstore.DBSession, column string) (int, error) { |
||||
var rows []map[string]string |
||||
|
||||
session.Cols("id", column, "secure_json_data") |
||||
session.Table("data_source") |
||||
session.In("type", datasourceTypes) |
||||
session.Where(column + " IS NOT NULL AND " + column + " != ''") |
||||
err := session.Find(&rows) |
||||
|
||||
if err != nil { |
||||
return 0, errutil.Wrapf(err, "failed to select column: %s", column) |
||||
} |
||||
|
||||
rowsUpdated, err := updateRows(session, rows, column) |
||||
return rowsUpdated, errutil.Wrapf(err, "failed to update column: %s", column) |
||||
} |
||||
|
||||
func updateRows(session *sqlstore.DBSession, rows []map[string]string, passwordFieldName string) (int, error) { |
||||
var rowsUpdated int |
||||
|
||||
for _, row := range rows { |
||||
newSecureJSONData, err := getUpdatedSecureJSONData(row, passwordFieldName) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
data, err := json.Marshal(newSecureJSONData) |
||||
if err != nil { |
||||
return 0, errutil.Wrap("marshaling newSecureJsonData failed", err) |
||||
} |
||||
|
||||
newRow := map[string]interface{}{"secure_json_data": data, passwordFieldName: ""} |
||||
session.Table("data_source") |
||||
session.Where("id = ?", row["id"]) |
||||
// Setting both columns while having value only for secure_json_data should clear the [passwordFieldName] column
|
||||
session.Cols("secure_json_data", passwordFieldName) |
||||
|
||||
_, err = session.Update(newRow) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
rowsUpdated++ |
||||
} |
||||
return rowsUpdated, nil |
||||
} |
||||
|
||||
func getUpdatedSecureJSONData(row map[string]string, passwordFieldName string) (map[string]interface{}, error) { |
||||
encryptedPassword, err := util.Encrypt([]byte(row[passwordFieldName]), setting.SecretKey) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var secureJSONData map[string]interface{} |
||||
|
||||
if err := json.Unmarshal([]byte(row["secure_json_data"]), &secureJSONData); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
jsonFieldName := util.ToCamelCase(passwordFieldName) |
||||
secureJSONData[jsonFieldName] = encryptedPassword |
||||
return secureJSONData, nil |
||||
} |
||||
@ -0,0 +1,67 @@ |
||||
package datamigrations |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/commandstest" |
||||
"github.com/grafana/grafana/pkg/components/securejsondata" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestPasswordMigrationCommand(t *testing.T) { |
||||
//setup datasources with password, basic_auth and none
|
||||
sqlstore := sqlstore.InitTestDB(t) |
||||
session := sqlstore.NewSession() |
||||
defer session.Close() |
||||
|
||||
datasources := []*models.DataSource{ |
||||
{Type: "influxdb", Name: "influxdb", Password: "foobar"}, |
||||
{Type: "graphite", Name: "graphite", BasicAuthPassword: "foobar"}, |
||||
{Type: "prometheus", Name: "prometheus", SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{})}, |
||||
} |
||||
|
||||
// set required default values
|
||||
for _, ds := range datasources { |
||||
ds.Created = time.Now() |
||||
ds.Updated = time.Now() |
||||
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{}) |
||||
} |
||||
|
||||
_, err := session.Insert(&datasources) |
||||
assert.Nil(t, err) |
||||
|
||||
//run migration
|
||||
err = EncryptDatasourcePaswords(&commandstest.FakeCommandLine{}, sqlstore) |
||||
assert.Nil(t, err) |
||||
|
||||
//verify that no datasources still have password or basic_auth
|
||||
var dss []*models.DataSource |
||||
err = session.SQL("select * from data_source").Find(&dss) |
||||
assert.Nil(t, err) |
||||
assert.Equal(t, len(dss), 3) |
||||
|
||||
for _, ds := range dss { |
||||
sj := ds.SecureJsonData.Decrypt() |
||||
|
||||
if ds.Name == "influxdb" { |
||||
assert.Equal(t, ds.Password, "") |
||||
v, exist := sj["password"] |
||||
assert.True(t, exist) |
||||
assert.Equal(t, v, "foobar", "expected password to be moved to securejson") |
||||
} |
||||
|
||||
if ds.Name == "graphite" { |
||||
assert.Equal(t, ds.BasicAuthPassword, "") |
||||
v, exist := sj["basicAuthPassword"] |
||||
assert.True(t, exist) |
||||
assert.Equal(t, v, "foobar", "expected basic_auth_password to be moved to securejson") |
||||
} |
||||
|
||||
if ds.Name == "prometheus" { |
||||
assert.Equal(t, len(sj), 0) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue