Chore: Refactor securedata to remove global encryption calls from dashboard snapshots (#38714)

* Add encryption service

* Add tests for encryption service

* Inject encryption service into http server

* Replace encryption global function usage in login tests

* Migrate to Wire

* Move Encryption bindings to OSS Wire set

* Chore: Refactor securedata to remove global encryption calls from dashboard snapshots

* Fix dashboard snapshot tests

* Remove no longer user test

* Add dashboard snapshots service tests

* Refactor service initialization

* Set up dashboard snapshots service as a background service

Co-authored-by: Tania B <yalyna.ts@gmail.com>
Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
Joan López de la Franca Beltran 2021-09-01 13:05:15 +02:00 committed by GitHub
parent a4e253bcf9
commit 6cfb640a0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 209 additions and 143 deletions

View File

@ -160,13 +160,8 @@ func GetDashboardSnapshot(c *models.ReqContext) response.Response {
return response.Error(404, "Dashboard snapshot not found", err)
}
dashboard, err := snapshot.DashboardJSON()
if err != nil {
return response.Error(500, "Failed to get dashboard data for dashboard snapshot", err)
}
dto := dtos.DashboardFullWithMeta{
Dashboard: dashboard,
Dashboard: snapshot.Dashboard,
Meta: dtos.DashboardMeta{
Type: models.DashTypeSnapshot,
IsSnapshot: true,
@ -256,11 +251,7 @@ func DeleteDashboardSnapshot(c *models.ReqContext) response.Response {
return response.Error(404, "Failed to get dashboard snapshot", nil)
}
dashboard, err := query.Result.DashboardJSON()
if err != nil {
return response.Error(500, "Failed to get dashboard data for dashboard snapshot", err)
}
dashboardID := dashboard.Get("id").MustInt64()
dashboardID := query.Result.Dashboard.Get("id").MustInt64()
guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
canEdit, err := guardian.CanEdit()

View File

@ -8,14 +8,11 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/securedata"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
@ -250,46 +247,5 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
assert.Equal(t, int64(100), id.MustInt64())
})
loggedInUserScenarioWithRole(t, "Should be able to read a snapshot's encrypted data When calling GET on",
"GET", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
origSecret := setting.SecretKey
setting.SecretKey = "dashboard_snapshot_api_test"
t.Cleanup(func() {
setting.SecretKey = origSecret
})
const dashboardID int64 = 123
jsonModel, err := simplejson.NewJson([]byte(fmt.Sprintf(`{"id":%d}`, dashboardID)))
require.NoError(t, err)
jsonModelEncoded, err := jsonModel.Encode()
require.NoError(t, err)
encrypted, err := securedata.Encrypt(jsonModelEncoded)
require.NoError(t, err)
// mock snapshot with encrypted dashboard info
mockSnapshotResult := &models.DashboardSnapshot{
Key: "12345",
DashboardEncrypted: encrypted,
Expires: time.Now().Add(time.Duration(1000) * time.Second),
}
setUpSnapshotTest(t)
bus.AddHandler("test", func(query *models.GetDashboardSnapshotQuery) error {
query.Result = mockSnapshotResult
return nil
})
sc.handlerFunc = GetDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, 200, sc.resp.Code)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
require.NoError(t, err)
assert.Equal(t, dashboardID, respJSON.Get("dashboard").Get("id").MustInt64())
})
})
}

View File

@ -1,16 +0,0 @@
package securedata
import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type SecureData []byte
func Encrypt(data []byte) (SecureData, error) {
return util.Encrypt(data, setting.SecretKey)
}
func (s SecureData) Decrypt() ([]byte, error) {
return util.Decrypt(s, setting.SecretKey)
}

View File

@ -3,7 +3,6 @@ package models
import (
"time"
"github.com/grafana/grafana/pkg/components/securedata"
"github.com/grafana/grafana/pkg/components/simplejson"
)
@ -24,18 +23,7 @@ type DashboardSnapshot struct {
Updated time.Time
Dashboard *simplejson.Json
DashboardEncrypted securedata.SecureData
}
func (ds *DashboardSnapshot) DashboardJSON() (*simplejson.Json, error) {
if ds.DashboardEncrypted != nil {
decrypted, err := ds.DashboardEncrypted.Decrypt()
if err != nil {
return nil, err
}
return simplejson.NewJson(decrypted)
}
return ds.Dashboard, nil
DashboardEncrypted []byte
}
// DashboardSnapshotDTO without dashboard map
@ -72,6 +60,8 @@ type CreateDashboardSnapshotCommand struct {
OrgId int64 `json:"-"`
UserId int64 `json:"-"`
DashboardEncrypted []byte `json:"-"`
Result *DashboardSnapshot
}

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/ngalert"
@ -39,8 +40,9 @@ func ProvideBackgroundServiceRegistry(
backendPM *backendmanager.Manager, metrics *metrics.InternalMetricsService,
usageStats *usagestats.UsageStatsService, tracing *tracing.TracingService, remoteCache *remotecache.RemoteCache,
// Need to make sure these are initialized, is there a better place to put them?
_ *azuremonitor.Service, _ *cloudwatch.CloudWatchService, _ *elasticsearch.Service, _ *graphite.Service, _ *influxdb.Service,
_ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service, _ *testdatasource.TestDataPlugin, _ *plugindashboards.Service,
_ *azuremonitor.Service, _ *cloudwatch.CloudWatchService, _ *elasticsearch.Service, _ *graphite.Service,
_ *influxdb.Service, _ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service,
_ *testdatasource.TestDataPlugin, _ *plugindashboards.Service, _ *dashboardsnapshots.Service,
) *BackgroundServiceRegistry {
return NewBackgroundServiceRegistry(

View File

@ -30,6 +30,7 @@ import (
"github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/libraryelements"
@ -135,6 +136,7 @@ var wireBasicSet = wire.NewSet(
graphite.ProvideService,
prometheus.ProvideService,
elasticsearch.ProvideService,
dashboardsnapshots.ProvideService,
)
var wireSet = wire.NewSet(

View File

@ -0,0 +1,83 @@
package dashboardsnapshots
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type Service struct {
Bus bus.Bus
SQLStore *sqlstore.SQLStore
EncryptionService encryption.Service
}
func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, encryptionService encryption.Service) *Service {
s := &Service{
Bus: bus,
SQLStore: store,
EncryptionService: encryptionService,
}
s.Bus.AddHandler(s.CreateDashboardSnapshot)
s.Bus.AddHandler(s.GetDashboardSnapshot)
s.Bus.AddHandler(s.DeleteDashboardSnapshot)
s.Bus.AddHandler(s.SearchDashboardSnapshots)
s.Bus.AddHandler(s.DeleteExpiredSnapshots)
return s
}
func (s *Service) CreateDashboardSnapshot(cmd *models.CreateDashboardSnapshotCommand) error {
marshalledData, err := cmd.Dashboard.Encode()
if err != nil {
return err
}
encryptedDashboard, err := s.EncryptionService.Encrypt(marshalledData, setting.SecretKey)
if err != nil {
return err
}
cmd.DashboardEncrypted = encryptedDashboard
return s.SQLStore.CreateDashboardSnapshot(cmd)
}
func (s *Service) GetDashboardSnapshot(query *models.GetDashboardSnapshotQuery) error {
err := s.SQLStore.GetDashboardSnapshot(query)
if err != nil {
return err
}
if query.Result.DashboardEncrypted != nil {
decryptedDashboard, err := s.EncryptionService.Decrypt(query.Result.DashboardEncrypted, setting.SecretKey)
if err != nil {
return err
}
dashboard, err := simplejson.NewJson(decryptedDashboard)
if err != nil {
return err
}
query.Result.Dashboard = dashboard
}
return err
}
func (s *Service) DeleteDashboardSnapshot(cmd *models.DeleteDashboardSnapshotCommand) error {
return s.SQLStore.DeleteDashboardSnapshot(cmd)
}
func (s *Service) SearchDashboardSnapshots(query *models.GetDashboardSnapshotsQuery) error {
return s.SQLStore.SearchDashboardSnapshots(query)
}
func (s *Service) DeleteExpiredSnapshots(cmd *models.DeleteExpiredSnapshotsCommand) error {
return s.SQLStore.DeleteExpiredSnapshots(cmd)
}

View File

@ -0,0 +1,64 @@
package dashboardsnapshots
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestDashboardSnapshotsService(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
s := &Service{
SQLStore: sqlStore,
EncryptionService: ossencryption.ProvideService(),
}
origSecret := setting.SecretKey
setting.SecretKey = "dashboard_snapshot_service_test"
t.Cleanup(func() {
setting.SecretKey = origSecret
})
dashboardKey := "12345"
rawDashboard := []byte(`{"id":123}`)
dashboard, err := simplejson.NewJson(rawDashboard)
require.NoError(t, err)
t.Run("create dashboard snapshot should encrypt the dashboard", func(t *testing.T) {
cmd := models.CreateDashboardSnapshotCommand{
Key: dashboardKey,
DeleteKey: dashboardKey,
Dashboard: dashboard,
}
err = s.CreateDashboardSnapshot(&cmd)
require.NoError(t, err)
decrypted, err := s.EncryptionService.Decrypt(cmd.Result.DashboardEncrypted, setting.SecretKey)
require.NoError(t, err)
require.Equal(t, rawDashboard, decrypted)
})
t.Run("get dashboard snapshot should return the dashboard decrypted", func(t *testing.T) {
query := models.GetDashboardSnapshotQuery{
Key: dashboardKey,
DeleteKey: dashboardKey,
}
err := s.GetDashboardSnapshot(&query)
require.NoError(t, err)
decrypted, err := query.Result.Dashboard.Encode()
require.NoError(t, err)
require.Equal(t, rawDashboard, decrypted)
})
}

View File

@ -3,25 +3,15 @@ package sqlstore
import (
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/securedata"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func init() {
bus.AddHandler("sql", CreateDashboardSnapshot)
bus.AddHandler("sql", GetDashboardSnapshot)
bus.AddHandler("sql", DeleteDashboardSnapshot)
bus.AddHandler("sql", SearchDashboardSnapshots)
bus.AddHandler("sql", DeleteExpiredSnapshots)
}
// DeleteExpiredSnapshots removes snapshots with old expiry dates.
// SnapShotRemoveExpired is deprecated and should be removed in the future.
// Snapshot expiry is decided by the user when they share the snapshot.
func DeleteExpiredSnapshots(cmd *models.DeleteExpiredSnapshotsCommand) error {
func (ss *SQLStore) DeleteExpiredSnapshots(cmd *models.DeleteExpiredSnapshotsCommand) error {
return inTransaction(func(sess *DBSession) error {
if !setting.SnapShotRemoveExpired {
sqlog.Warn("[Deprecated] The snapshot_remove_expired setting is outdated. Please remove from your config.")
@ -39,7 +29,7 @@ func DeleteExpiredSnapshots(cmd *models.DeleteExpiredSnapshotsCommand) error {
})
}
func CreateDashboardSnapshot(cmd *models.CreateDashboardSnapshotCommand) error {
func (ss *SQLStore) CreateDashboardSnapshot(cmd *models.CreateDashboardSnapshotCommand) error {
return inTransaction(func(sess *DBSession) error {
// never
var expires = time.Now().Add(time.Hour * 24 * 365 * 50)
@ -47,16 +37,6 @@ func CreateDashboardSnapshot(cmd *models.CreateDashboardSnapshotCommand) error {
expires = time.Now().Add(time.Second * time.Duration(cmd.Expires))
}
marshalledData, err := cmd.Dashboard.Encode()
if err != nil {
return err
}
encryptedDashboard, err := securedata.Encrypt(marshalledData)
if err != nil {
return err
}
snapshot := &models.DashboardSnapshot{
Name: cmd.Name,
Key: cmd.Key,
@ -67,19 +47,19 @@ func CreateDashboardSnapshot(cmd *models.CreateDashboardSnapshotCommand) error {
ExternalUrl: cmd.ExternalUrl,
ExternalDeleteUrl: cmd.ExternalDeleteUrl,
Dashboard: simplejson.New(),
DashboardEncrypted: encryptedDashboard,
DashboardEncrypted: cmd.DashboardEncrypted,
Expires: expires,
Created: time.Now(),
Updated: time.Now(),
}
_, err = sess.Insert(snapshot)
_, err := sess.Insert(snapshot)
cmd.Result = snapshot
return err
})
}
func DeleteDashboardSnapshot(cmd *models.DeleteDashboardSnapshotCommand) error {
func (ss *SQLStore) DeleteDashboardSnapshot(cmd *models.DeleteDashboardSnapshotCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSQL = "DELETE FROM dashboard_snapshot WHERE delete_key=?"
_, err := sess.Exec(rawSQL, cmd.DeleteKey)
@ -87,7 +67,7 @@ func DeleteDashboardSnapshot(cmd *models.DeleteDashboardSnapshotCommand) error {
})
}
func GetDashboardSnapshot(query *models.GetDashboardSnapshotQuery) error {
func (ss *SQLStore) GetDashboardSnapshot(query *models.GetDashboardSnapshotQuery) error {
snapshot := models.DashboardSnapshot{Key: query.Key, DeleteKey: query.DeleteKey}
has, err := x.Get(&snapshot)
@ -103,7 +83,7 @@ func GetDashboardSnapshot(query *models.GetDashboardSnapshotQuery) error {
// SearchDashboardSnapshots returns a list of all snapshots for admins
// for other roles, it returns snapshots created by the user
func SearchDashboardSnapshots(query *models.GetDashboardSnapshotsQuery) error {
func (ss *SQLStore) SearchDashboardSnapshots(query *models.GetDashboardSnapshotsQuery) error {
var snapshots = make(models.DashboardSnapshotsList, 0)
sess := x.NewSession()

View File

@ -6,16 +6,16 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDashboardSnapshotDBAccess(t *testing.T) {
InitTestDB(t)
sqlstore := InitTestDB(t)
origSecret := setting.SecretKey
setting.SecretKey = "dashboard_snapshot_testing"
@ -23,27 +23,41 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
setting.SecretKey = origSecret
})
dashboard := simplejson.NewFromAny(map[string]interface{}{"hello": "mupp"})
t.Run("Given saved snapshot", func(t *testing.T) {
rawDashboard, err := dashboard.Encode()
require.NoError(t, err)
encryptedDashboard, err := ossencryption.ProvideService().Encrypt(rawDashboard, setting.SecretKey)
require.NoError(t, err)
cmd := models.CreateDashboardSnapshotCommand{
Key: "hej",
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"hello": "mupp",
}),
UserId: 1000,
OrgId: 1,
Key: "hej",
DashboardEncrypted: encryptedDashboard,
UserId: 1000,
OrgId: 1,
}
err := CreateDashboardSnapshot(&cmd)
err = sqlstore.CreateDashboardSnapshot(&cmd)
require.NoError(t, err)
t.Run("Should be able to get snapshot by key", func(t *testing.T) {
query := models.GetDashboardSnapshotQuery{Key: "hej"}
err := GetDashboardSnapshot(&query)
err := sqlstore.GetDashboardSnapshot(&query)
require.NoError(t, err)
assert.NotNil(t, query.Result)
dashboard, err := query.Result.DashboardJSON()
decryptedDashboard, err := ossencryption.ProvideService().Decrypt(
query.Result.DashboardEncrypted,
setting.SecretKey,
)
require.NoError(t, err)
dashboard, err := simplejson.NewJson(decryptedDashboard)
require.NoError(t, err)
assert.Equal(t, "mupp", dashboard.Get("hello").MustString())
})
@ -52,7 +66,7 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
OrgId: 1,
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_ADMIN},
}
err := SearchDashboardSnapshots(&query)
err := sqlstore.SearchDashboardSnapshots(&query)
require.NoError(t, err)
t.Run("Should return all the snapshots", func(t *testing.T) {
@ -66,7 +80,7 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
OrgId: 1,
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR, UserId: 1000},
}
err := SearchDashboardSnapshots(&query)
err := sqlstore.SearchDashboardSnapshots(&query)
require.NoError(t, err)
t.Run("Should return all the snapshots", func(t *testing.T) {
@ -80,7 +94,7 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
OrgId: 1,
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR, UserId: 2},
}
err := SearchDashboardSnapshots(&query)
err := sqlstore.SearchDashboardSnapshots(&query)
require.NoError(t, err)
t.Run("Should not return any snapshots", func(t *testing.T) {
@ -99,7 +113,7 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
UserId: 0,
OrgId: 1,
}
err := CreateDashboardSnapshot(&cmd)
err := sqlstore.CreateDashboardSnapshot(&cmd)
require.NoError(t, err)
t.Run("Should not return any snapshots", func(t *testing.T) {
@ -107,7 +121,7 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
OrgId: 1,
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR, IsAnonymous: true, UserId: 0},
}
err := SearchDashboardSnapshots(&query)
err := sqlstore.SearchDashboardSnapshots(&query)
require.NoError(t, err)
require.NotNil(t, query.Result)
@ -116,13 +130,13 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
})
t.Run("Should have encrypted dashboard data", func(t *testing.T) {
original, err := cmd.Dashboard.Encode()
decryptedDashboard, err := ossencryption.ProvideService().Decrypt(
cmd.Result.DashboardEncrypted,
setting.SecretKey,
)
require.NoError(t, err)
decrypted, err := cmd.Result.DashboardEncrypted.Decrypt()
require.NoError(t, err)
require.Equal(t, decrypted, original)
require.Equal(t, decryptedDashboard, rawDashboard)
})
})
}
@ -137,27 +151,27 @@ func TestDeleteExpiredSnapshots(t *testing.T) {
createTestSnapshot(t, sqlstore, "key2", -1200)
createTestSnapshot(t, sqlstore, "key3", -1200)
err := DeleteExpiredSnapshots(&models.DeleteExpiredSnapshotsCommand{})
err := sqlstore.DeleteExpiredSnapshots(&models.DeleteExpiredSnapshotsCommand{})
require.NoError(t, err)
query := models.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_ADMIN},
}
err = SearchDashboardSnapshots(&query)
err = sqlstore.SearchDashboardSnapshots(&query)
require.NoError(t, err)
assert.Len(t, query.Result, 1)
assert.Equal(t, nonExpiredSnapshot.Key, query.Result[0].Key)
err = DeleteExpiredSnapshots(&models.DeleteExpiredSnapshotsCommand{})
err = sqlstore.DeleteExpiredSnapshots(&models.DeleteExpiredSnapshotsCommand{})
require.NoError(t, err)
query = models.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_ADMIN},
}
err = SearchDashboardSnapshots(&query)
err = sqlstore.SearchDashboardSnapshots(&query)
require.NoError(t, err)
require.Len(t, query.Result, 1)
@ -176,7 +190,7 @@ func createTestSnapshot(t *testing.T, sqlstore *SQLStore, key string, expires in
OrgId: 1,
Expires: expires,
}
err := CreateDashboardSnapshot(&cmd)
err := sqlstore.CreateDashboardSnapshot(&cmd)
require.NoError(t, err)
// Set expiry date manually - to be able to create expired snapshots