CloudMigrations: Store encryption key in unified secrets table (#90908)

* store encryption key in unified secrets table

* fix local dev mode

* make metadata more realistic

* fix tests

* fix sql queries against postgres

* fix stats endpoint
pull/90636/head^2
Michael Mandrus 10 months ago committed by GitHub
parent 49c756d774
commit dc355331a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      pkg/services/cloudmigration/api/api.go
  2. 2
      pkg/services/cloudmigration/api/api_test.go
  3. 1
      pkg/services/cloudmigration/api/dtos.go
  4. 4
      pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go
  5. 2
      pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go
  6. 50
      pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go
  7. 2
      pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go
  8. 23
      pkg/services/cloudmigration/gmsclient/inmemory_client.go
  9. 2
      pkg/services/cloudmigration/model.go
  10. 4
      public/api-merged.json
  11. 4
      public/openapi3.json

@ -445,6 +445,7 @@ func (cma *CloudMigrationAPI) GetSnapshot(c *contextmodel.ReqContext) response.R
dtoStats := SnapshotResourceStats{ dtoStats := SnapshotResourceStats{
Types: make(map[MigrateDataType]int, len(snapshot.StatsRollup.CountsByStatus)), Types: make(map[MigrateDataType]int, len(snapshot.StatsRollup.CountsByStatus)),
Statuses: make(map[ItemStatus]int, len(snapshot.StatsRollup.CountsByType)), Statuses: make(map[ItemStatus]int, len(snapshot.StatsRollup.CountsByType)),
Total: snapshot.StatsRollup.Total,
} }
for s, c := range snapshot.StatsRollup.CountsByStatus { for s, c := range snapshot.StatsRollup.CountsByStatus {
dtoStats.Statuses[ItemStatus(s)] = c dtoStats.Statuses[ItemStatus(s)] = c

@ -471,7 +471,7 @@ func TestCloudMigrationAPI_GetSnapshot(t *testing.T) {
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1", requestUrl: "/api/cloudmigration/migration/1234/snapshot/1",
basicRole: org.RoleAdmin, basicRole: org.RoleAdmin,
expectedHttpResult: http.StatusOK, expectedHttpResult: http.StatusOK,
expectedBody: `{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z","results":[],"stats":{"types":{},"statuses":{}}}`, expectedBody: `{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z","results":[],"stats":{"types":{},"statuses":{},"total":0}}`,
}, },
{ {
desc: "should return 403 if no used is not admin", desc: "should return 403 if no used is not admin",

@ -307,6 +307,7 @@ type GetSnapshotResponseDTO struct {
type SnapshotResourceStats struct { type SnapshotResourceStats struct {
Types map[MigrateDataType]int `json:"types"` Types map[MigrateDataType]int `json:"types"`
Statuses map[ItemStatus]int `json:"statuses"` Statuses map[ItemStatus]int `json:"statuses"`
Total int `json:"total"`
} }
// swagger:parameters getShapshotList // swagger:parameters getShapshotList

@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/gcom" "github.com/grafana/grafana/pkg/services/gcom"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -82,6 +83,7 @@ func ProvideService(
features featuremgmt.FeatureToggles, features featuremgmt.FeatureToggles,
db db.DB, db db.DB,
dsService datasources.DataSourceService, dsService datasources.DataSourceService,
secretsStore secretskv.SecretsKVStore,
secretsService secrets.Service, secretsService secrets.Service,
routeRegister routing.RouteRegister, routeRegister routing.RouteRegister,
prom prometheus.Registerer, prom prometheus.Registerer,
@ -95,7 +97,7 @@ func ProvideService(
} }
s := &Service{ s := &Service{
store: &sqlStore{db: db, secretsService: secretsService}, store: &sqlStore{db: db, secretsStore: secretsStore, secretsService: secretsService},
log: log.New(LogPrefix), log: log.New(LogPrefix),
cfg: cfg, cfg: cfg,
features: features, features: features,

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/folder/foldertest"
secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -438,6 +439,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi
featuremgmt.FlagDashboardRestore), featuremgmt.FlagDashboardRestore),
sqlStore, sqlStore,
dsService, dsService,
secretskv.NewFakeSQLSecretsKVStore(t),
secretsService, secretsService,
rr, rr,
prometheus.DefaultRegisterer, prometheus.DefaultRegisterer,

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/cloudmigration" "github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -17,11 +18,13 @@ var _ store = (*sqlStore)(nil)
type sqlStore struct { type sqlStore struct {
db db.DB db db.DB
secretsStore secretskv.SecretsKVStore
secretsService secrets.Service secretsService secrets.Service
} }
const ( const (
tableName = "cloud_migration_resource" tableName = "cloud_migration_resource"
secretType = "cloudmigration-snapshot-encryption-key"
) )
func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) {
@ -157,14 +160,14 @@ func (ss *sqlStore) GetMigrationStatusList(ctx context.Context, migrationUID str
} }
func (ss *sqlStore) CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) (string, error) { func (ss *sqlStore) CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) (string, error) {
if err := ss.encryptKey(ctx, &snapshot); err != nil {
return "", err
}
if snapshot.UID == "" { if snapshot.UID == "" {
snapshot.UID = util.GenerateShortUID() snapshot.UID = util.GenerateShortUID()
} }
if err := ss.secretsStore.Set(ctx, secretskv.AllOrganizations, snapshot.UID, secretType, string(snapshot.EncryptionKey)); err != nil {
return "", err
}
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
snapshot.Created = time.Now() snapshot.Created = time.Now()
snapshot.Updated = time.Now() snapshot.Updated = time.Now()
@ -228,8 +231,12 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, uid string, resultPage
return nil, err return nil, err
} }
if err := ss.decryptKey(ctx, &snapshot); err != nil { if secret, found, err := ss.secretsStore.Get(ctx, secretskv.AllOrganizations, snapshot.UID, secretType); err != nil {
return &snapshot, err return &snapshot, err
} else if !found {
return &snapshot, fmt.Errorf("encryption key not found for snapshot with UID %s", snapshot.UID)
} else {
snapshot.EncryptionKey = []byte(secret)
} }
resources, err := ss.GetSnapshotResources(ctx, uid, resultPage, resultLimit) resources, err := ss.GetSnapshotResources(ctx, uid, resultPage, resultLimit)
@ -259,8 +266,12 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li
return nil, err return nil, err
} }
for i, snapshot := range snapshots { for i, snapshot := range snapshots {
if err := ss.decryptKey(ctx, &snapshot); err != nil { if secret, found, err := ss.secretsStore.Get(ctx, secretskv.AllOrganizations, snapshot.UID, secretType); err != nil {
return nil, err return nil, err
} else if !found {
return nil, fmt.Errorf("encryption key not found for snapshot with UID %s", snapshot.UID)
} else {
snapshot.EncryptionKey = []byte(secret)
} }
if stats, err := ss.GetSnapshotResourceStats(ctx, snapshot.UID); err != nil { if stats, err := ss.GetSnapshotResourceStats(ctx, snapshot.UID); err != nil {
@ -346,14 +357,14 @@ func (ss *sqlStore) GetSnapshotResourceStats(ctx context.Context, snapshotUid st
} else { } else {
total = int(t) total = int(t)
} }
sess.Select("count(uid) as 'count', resource_type as 'type'"). sess.Select("count(uid) as \"count\", resource_type as \"type\"").
Table(tableName). Table(tableName).
GroupBy("type"). GroupBy("type").
Where("snapshot_uid = ?", snapshotUid) Where("snapshot_uid = ?", snapshotUid)
if err := sess.Find(&typeCounts); err != nil { if err := sess.Find(&typeCounts); err != nil {
return err return err
} }
sess.Select("count(uid) as 'count', status"). sess.Select("count(uid) as \"count\", status").
Table(tableName). Table(tableName).
GroupBy("status"). GroupBy("status").
Where("snapshot_uid = ?", snapshotUid) Where("snapshot_uid = ?", snapshotUid)
@ -411,24 +422,3 @@ func (ss *sqlStore) decryptToken(ctx context.Context, cm *cloudmigration.CloudMi
return nil return nil
} }
func (ss *sqlStore) encryptKey(ctx context.Context, snapshot *cloudmigration.CloudMigrationSnapshot) error {
s, err := ss.secretsService.Encrypt(ctx, snapshot.EncryptionKey, secrets.WithoutScope())
if err != nil {
return fmt.Errorf("encrypting key: %w", err)
}
snapshot.EncryptionKey = s
return nil
}
func (ss *sqlStore) decryptKey(ctx context.Context, snapshot *cloudmigration.CloudMigrationSnapshot) error {
t, err := ss.secretsService.Decrypt(ctx, snapshot.EncryptionKey)
if err != nil {
return fmt.Errorf("decrypting key: %w", err)
}
snapshot.EncryptionKey = t
return nil
}

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/cloudmigration" "github.com/grafana/grafana/pkg/services/cloudmigration"
fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes" fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -274,6 +275,7 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) {
s := &sqlStore{ s := &sqlStore{
db: testDB, db: testDB,
secretsService: fakeSecrets.FakeSecretsService{}, secretsService: fakeSecrets.FakeSecretsService{},
secretsStore: secretskv.NewFakeSQLSecretsKVStore(t),
} }
ctx := context.Background() ctx := context.Background()

@ -2,6 +2,7 @@ package gmsclient
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"math/rand" "math/rand"
@ -51,16 +52,34 @@ func (c *memoryClientImpl) MigrateData(
return &result, nil return &result, nil
} }
func (c *memoryClientImpl) StartSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error) { func (c *memoryClientImpl) StartSnapshot(_ context.Context, sess cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error) {
publicKey, _, err := box.GenerateKey(cryptoRand.Reader) publicKey, _, err := box.GenerateKey(cryptoRand.Reader)
if err != nil { if err != nil {
return nil, fmt.Errorf("nacl: generating public and private key: %w", err) return nil, fmt.Errorf("nacl: generating public and private key: %w", err)
} }
snapshotUid := uuid.NewString()
metadataBuffer, err := json.Marshal(struct {
SnapshotID string `json:"snapshotID"`
StackID string `json:"stackID"`
Slug string `json:"slug"`
}{
SnapshotID: snapshotUid,
StackID: fmt.Sprintf("%d", sess.StackID),
Slug: sess.Slug,
})
if err != nil {
return nil, fmt.Errorf("marshalling metadata: %w", err)
}
c.snapshot = &cloudmigration.StartSnapshotResponse{ c.snapshot = &cloudmigration.StartSnapshotResponse{
EncryptionKey: publicKey[:], EncryptionKey: publicKey[:],
SnapshotID: uuid.NewString(), SnapshotID: snapshotUid,
MaxItemsPerPartition: 10, MaxItemsPerPartition: 10,
Algo: "nacl", Algo: "nacl",
Metadata: metadataBuffer,
} }
return c.snapshot, nil return c.snapshot, nil

@ -37,7 +37,7 @@ type CloudMigrationSnapshot struct {
UID string `xorm:"uid"` UID string `xorm:"uid"`
SessionUID string `xorm:"session_uid"` SessionUID string `xorm:"session_uid"`
Status SnapshotStatus Status SnapshotStatus
EncryptionKey []byte `xorm:"encryption_key"` // stored in the unified secrets table EncryptionKey []byte `xorm:"-"` // stored in the unified secrets table
LocalDir string `xorm:"local_directory"` LocalDir string `xorm:"local_directory"`
GMSSnapshotUID string `xorm:"gms_snapshot_uid"` GMSSnapshotUID string `xorm:"gms_snapshot_uid"`
ErrorString string `xorm:"error_string"` ErrorString string `xorm:"error_string"`

@ -20421,6 +20421,10 @@
"format": "int64" "format": "int64"
} }
}, },
"total": {
"type": "integer",
"format": "int64"
},
"types": { "types": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {

@ -10497,6 +10497,10 @@
}, },
"type": "object" "type": "object"
}, },
"total": {
"format": "int64",
"type": "integer"
},
"types": { "types": {
"additionalProperties": { "additionalProperties": {
"format": "int64", "format": "int64",

Loading…
Cancel
Save