mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Encryption: Add support for multiple data keys per day (#47765)
* Add database migrations * Use short uids as data key ids * Add support for manual data key rotation * Fix duplicated mutex unlocks * Fix migration * Manage current data keys per name * Adjust key re-encryption and test * Modify rename column migration for MySQL compatibility * Refactor secrets manager and data keys cache * Multiple o11y adjustments * Fix stats query * Apply suggestions from code review Co-authored-by: Tania <yalyna.ts@gmail.com> * Fix linter * Docs: Rotate data encryption keys API endpoint Co-authored-by: Tania <yalyna.ts@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ae8c11bfa4
commit
e43879e55d
@@ -704,3 +704,28 @@ Content-Type: application/json
|
||||
"message": "LDAP config reloaded"
|
||||
}
|
||||
```
|
||||
|
||||
## Rotate data encryption keys
|
||||
|
||||
`POST /api/admin/encryption/rotate-data-keys`
|
||||
|
||||
Rotates data encryption keys, so all the active keys are disabled
|
||||
and no longer used for encryption but kept for decryption operations.
|
||||
|
||||
Secrets encrypted with one of the deactivated keys need to be re-encrypted
|
||||
to actually stop using those keys for both encryption and decryption.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/admin/encryption/rotate-data-keys HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 204
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
16
pkg/api/admin_encryption.go
Normal file
16
pkg/api/admin_encryption.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response {
|
||||
if err := hs.SecretsService.RotateDataKeys(c.Req.Context()); err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to rotate data key", err)
|
||||
}
|
||||
|
||||
return response.Respond(http.StatusNoContent, "")
|
||||
}
|
@@ -558,6 +558,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
adminRoute.Post("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestExport))
|
||||
}
|
||||
|
||||
adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys))
|
||||
|
||||
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards))
|
||||
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
||||
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources))
|
||||
|
@@ -192,7 +192,7 @@ var (
|
||||
StatsTotalLibraryVariables prometheus.Gauge
|
||||
|
||||
// StatsTotalDataKeys is a metric of total number of data keys stored in Grafana.
|
||||
StatsTotalDataKeys prometheus.Gauge
|
||||
StatsTotalDataKeys *prometheus.GaugeVec
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -568,11 +568,11 @@ func init() {
|
||||
Namespace: ExporterName,
|
||||
})
|
||||
|
||||
StatsTotalDataKeys = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
StatsTotalDataKeys = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "stat_totals_data_keys",
|
||||
Help: "total amount of data keys in the database",
|
||||
Namespace: ExporterName,
|
||||
})
|
||||
}, []string{"active"})
|
||||
}
|
||||
|
||||
// SetBuildInformation sets the build information for this binary
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -139,6 +140,7 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) {
|
||||
m["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin
|
||||
m["stats.api_keys.count"] = statsQuery.Result.APIKeys
|
||||
m["stats.data_keys.count"] = statsQuery.Result.DataKeys
|
||||
m["stats.active_data_keys.count"] = statsQuery.Result.ActiveDataKeys
|
||||
|
||||
ossEditionCount := 1
|
||||
enterpriseEditionCount := 0
|
||||
@@ -327,7 +329,10 @@ func (s *Service) updateTotalStats(ctx context.Context) bool {
|
||||
metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
|
||||
metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
|
||||
metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
|
||||
metrics.StatsTotalDataKeys.Set(float64(statsQuery.Result.DataKeys))
|
||||
|
||||
metrics.StatsTotalDataKeys.With(prometheus.Labels{"active": "true"}).Set(float64(statsQuery.Result.ActiveDataKeys))
|
||||
inactiveDataKeys := statsQuery.Result.DataKeys - statsQuery.Result.ActiveDataKeys
|
||||
metrics.StatsTotalDataKeys.With(prometheus.Labels{"active": "false"}).Set(float64(inactiveDataKeys))
|
||||
|
||||
dsStats := models.GetDataSourceStatsQuery{}
|
||||
if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil {
|
||||
|
@@ -281,6 +281,7 @@ func TestCollectingUsageStats(t *testing.T) {
|
||||
assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"])
|
||||
|
||||
assert.EqualValues(t, 11, metrics["stats.data_keys.count"])
|
||||
assert.EqualValues(t, 3, metrics["stats.active_data_keys.count"])
|
||||
|
||||
assert.InDelta(t, int64(65), metrics["stats.uptime"], 6)
|
||||
}
|
||||
@@ -325,6 +326,7 @@ func mockSystemStats(sqlStore *mockstore.SQLStoreMock) {
|
||||
FoldersViewersCanEdit: 5,
|
||||
APIKeys: 2,
|
||||
DataKeys: 11,
|
||||
ActiveDataKeys: 3,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -40,6 +40,7 @@ type SystemStats struct {
|
||||
DailyActiveViewers int64
|
||||
DailyActiveSessions int64
|
||||
DataKeys int64
|
||||
ActiveDataKeys int64
|
||||
}
|
||||
|
||||
type DataSourceStats struct {
|
||||
|
@@ -26,7 +26,30 @@ func ProvideSecretsStore(sqlStore *sqlstore.SQLStore) *SecretsStoreImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, name string) (*secrets.DataKey, error) {
|
||||
func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, id string) (*secrets.DataKey, error) {
|
||||
dataKey := &secrets.DataKey{}
|
||||
var exists bool
|
||||
|
||||
err := ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var err error
|
||||
exists, err = sess.Table(dataKeysTable).
|
||||
Where("id = ?", id).
|
||||
Get(dataKey)
|
||||
return err
|
||||
})
|
||||
|
||||
if !exists {
|
||||
return nil, secrets.ErrDataKeyNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting data key: %w", err)
|
||||
}
|
||||
|
||||
return dataKey, nil
|
||||
}
|
||||
|
||||
func (ss *SecretsStoreImpl) GetCurrentDataKey(ctx context.Context, name string) (*secrets.DataKey, error) {
|
||||
dataKey := &secrets.DataKey{}
|
||||
var exists bool
|
||||
|
||||
@@ -43,7 +66,6 @@ func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, name string) (*secre
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ss.log.Error("Failed to get data key", "err", err, "name", name)
|
||||
return nil, fmt.Errorf("failed getting data key: %w", err)
|
||||
}
|
||||
|
||||
@@ -59,13 +81,13 @@ func (ss *SecretsStoreImpl) GetAllDataKeys(ctx context.Context) ([]*secrets.Data
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (ss *SecretsStoreImpl) CreateDataKey(ctx context.Context, dataKey secrets.DataKey) error {
|
||||
func (ss *SecretsStoreImpl) CreateDataKey(ctx context.Context, dataKey *secrets.DataKey) error {
|
||||
return ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
return ss.CreateDataKeyWithDBSession(ctx, dataKey, sess.Session)
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *SecretsStoreImpl) CreateDataKeyWithDBSession(_ context.Context, dataKey secrets.DataKey, sess *xorm.Session) error {
|
||||
func (ss *SecretsStoreImpl) CreateDataKeyWithDBSession(_ context.Context, dataKey *secrets.DataKey, sess *xorm.Session) error {
|
||||
if !dataKey.Active {
|
||||
return fmt.Errorf("cannot insert deactivated data keys")
|
||||
}
|
||||
@@ -73,17 +95,26 @@ func (ss *SecretsStoreImpl) CreateDataKeyWithDBSession(_ context.Context, dataKe
|
||||
dataKey.Created = time.Now()
|
||||
dataKey.Updated = dataKey.Created
|
||||
|
||||
_, err := sess.Table(dataKeysTable).Insert(&dataKey)
|
||||
_, err := sess.Table(dataKeysTable).Insert(dataKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ss *SecretsStoreImpl) DeleteDataKey(ctx context.Context, name string) error {
|
||||
if len(name) == 0 {
|
||||
return fmt.Errorf("data key name is missing")
|
||||
func (ss *SecretsStoreImpl) DisableDataKeys(ctx context.Context) error {
|
||||
return ss.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
_, err := sess.Table(dataKeysTable).
|
||||
Where("active = ?", ss.sqlStore.Dialect.BooleanStr(true)).
|
||||
UseBool("active").Update(&secrets.DataKey{Active: false})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *SecretsStoreImpl) DeleteDataKey(ctx context.Context, id string) error {
|
||||
if len(id) == 0 {
|
||||
return fmt.Errorf("data key id is missing")
|
||||
}
|
||||
|
||||
return ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
_, err := sess.Table(dataKeysTable).Delete(&secrets.DataKey{Name: name})
|
||||
_, err := sess.Table(dataKeysTable).Delete(&secrets.DataKey{Id: id})
|
||||
|
||||
return err
|
||||
})
|
||||
@@ -94,60 +125,71 @@ func (ss *SecretsStoreImpl) ReEncryptDataKeys(
|
||||
providers map[secrets.ProviderID]secrets.Provider,
|
||||
currProvider secrets.ProviderID,
|
||||
) error {
|
||||
return ss.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
keys := make([]*secrets.DataKey, 0)
|
||||
if err := sess.Table(dataKeysTable).Find(&keys); err != nil {
|
||||
return err
|
||||
}
|
||||
keys := make([]*secrets.DataKey, 0)
|
||||
if err := ss.sqlStore.NewSession(ctx).Table(dataKeysTable).Find(&keys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
for _, k := range keys {
|
||||
err := ss.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
provider, ok := providers[kmsproviders.NormalizeProviderID(k.Provider)]
|
||||
if !ok {
|
||||
ss.log.Warn(
|
||||
"Could not find provider to re-encrypt data encryption key",
|
||||
"key_id", k.Name,
|
||||
"id", k.Id,
|
||||
"name", k.Name,
|
||||
"provider", k.Provider,
|
||||
)
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
|
||||
decrypted, err := provider.Decrypt(ctx, k.EncryptedData)
|
||||
if err != nil {
|
||||
ss.log.Warn(
|
||||
"Error while decrypting data encryption key to re-encrypt it",
|
||||
"key_id", k.Name,
|
||||
"id", k.Id,
|
||||
"name", k.Name,
|
||||
"provider", k.Provider,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
|
||||
// Updating current data key by re-encrypting it with current provider.
|
||||
// Accessing the current provider within providers map should be safe.
|
||||
k.Provider = currProvider
|
||||
k.Name = secrets.KeyName(k.Scope, currProvider)
|
||||
k.Updated = time.Now()
|
||||
k.EncryptedData, err = providers[currProvider].Encrypt(ctx, decrypted)
|
||||
if err != nil {
|
||||
ss.log.Warn(
|
||||
"Error while re-encrypting data encryption key",
|
||||
"key_id", k.Name,
|
||||
"id", k.Id,
|
||||
"name", k.Name,
|
||||
"provider", k.Provider,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := sess.Table(dataKeysTable).Where("name = ?", k.Name).Update(k); err != nil {
|
||||
if _, err := sess.Table(dataKeysTable).Where("id = ?", k.Id).Update(k); err != nil {
|
||||
ss.log.Warn(
|
||||
"Error while re-encrypting data encryption key",
|
||||
"key_id", k.Name,
|
||||
"id", k.Id,
|
||||
"name", k.Name,
|
||||
"provider", k.Provider,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -40,6 +40,10 @@ func (f FakeSecretsService) GetDecryptedValue(_ context.Context, sjd map[string]
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (f FakeSecretsService) RotateDataKeys(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FakeSecretsService) ReEncryptDataKeys(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
@@ -15,14 +15,25 @@ func NewFakeSecretsStore() FakeSecretsStore {
|
||||
return FakeSecretsStore{store: make(map[string]*secrets.DataKey)}
|
||||
}
|
||||
|
||||
func (f FakeSecretsStore) GetDataKey(_ context.Context, name string) (*secrets.DataKey, error) {
|
||||
key, ok := f.store[name]
|
||||
func (f FakeSecretsStore) GetDataKey(_ context.Context, id string) (*secrets.DataKey, error) {
|
||||
key, ok := f.store[id]
|
||||
if !ok {
|
||||
return nil, secrets.ErrDataKeyNotFound
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (f FakeSecretsStore) GetCurrentDataKey(_ context.Context, name string) (*secrets.DataKey, error) {
|
||||
for _, key := range f.store {
|
||||
if key.Name == name && key.Active {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, secrets.ErrDataKeyNotFound
|
||||
}
|
||||
|
||||
func (f FakeSecretsStore) GetAllDataKeys(_ context.Context) ([]*secrets.DataKey, error) {
|
||||
result := make([]*secrets.DataKey, 0)
|
||||
for _, key := range f.store {
|
||||
@@ -31,18 +42,25 @@ func (f FakeSecretsStore) GetAllDataKeys(_ context.Context) ([]*secrets.DataKey,
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f FakeSecretsStore) CreateDataKey(_ context.Context, dataKey secrets.DataKey) error {
|
||||
f.store[dataKey.Name] = &dataKey
|
||||
func (f FakeSecretsStore) CreateDataKey(_ context.Context, dataKey *secrets.DataKey) error {
|
||||
f.store[dataKey.Id] = dataKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FakeSecretsStore) CreateDataKeyWithDBSession(_ context.Context, dataKey secrets.DataKey, _ *xorm.Session) error {
|
||||
f.store[dataKey.Name] = &dataKey
|
||||
func (f FakeSecretsStore) CreateDataKeyWithDBSession(_ context.Context, dataKey *secrets.DataKey, _ *xorm.Session) error {
|
||||
f.store[dataKey.Id] = dataKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FakeSecretsStore) DeleteDataKey(_ context.Context, name string) error {
|
||||
delete(f.store, name)
|
||||
func (f FakeSecretsStore) DisableDataKeys(_ context.Context) error {
|
||||
for id := range f.store {
|
||||
f.store[id].Active = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FakeSecretsStore) DeleteDataKey(_ context.Context, id string) error {
|
||||
delete(f.store, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,8 @@ var (
|
||||
)
|
||||
|
||||
type dataKeyCacheEntry struct {
|
||||
id string
|
||||
name string
|
||||
dataKey []byte
|
||||
expiration time.Time
|
||||
}
|
||||
@@ -22,58 +24,86 @@ func (e dataKeyCacheEntry) expired() bool {
|
||||
}
|
||||
|
||||
type dataKeyCache struct {
|
||||
sync.RWMutex
|
||||
entries map[string]dataKeyCacheEntry
|
||||
mtx sync.RWMutex
|
||||
byId map[string]*dataKeyCacheEntry
|
||||
byName map[string]*dataKeyCacheEntry
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
func newDataKeyCache(ttl time.Duration) *dataKeyCache {
|
||||
return &dataKeyCache{
|
||||
entries: make(map[string]dataKeyCacheEntry),
|
||||
byId: make(map[string]*dataKeyCacheEntry),
|
||||
byName: make(map[string]*dataKeyCacheEntry),
|
||||
cacheTTL: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *dataKeyCache) get(id string) ([]byte, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
func (c *dataKeyCache) getById(id string) (*dataKeyCacheEntry, bool) {
|
||||
c.mtx.RLock()
|
||||
defer c.mtx.RUnlock()
|
||||
|
||||
entry, exists := c.entries[id]
|
||||
entry, exists := c.byId[id]
|
||||
|
||||
cacheReadsCounter.With(prometheus.Labels{
|
||||
"hit": strconv.FormatBool(exists),
|
||||
"hit": strconv.FormatBool(exists),
|
||||
"method": "byId",
|
||||
}).Inc()
|
||||
|
||||
if !exists || entry.expired() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.dataKey, true
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (c *dataKeyCache) add(id string, dataKey []byte) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
func (c *dataKeyCache) getByName(name string) (*dataKeyCacheEntry, bool) {
|
||||
c.mtx.RLock()
|
||||
defer c.mtx.RUnlock()
|
||||
|
||||
c.entries[id] = dataKeyCacheEntry{
|
||||
dataKey: dataKey,
|
||||
expiration: now().Add(c.cacheTTL),
|
||||
entry, exists := c.byName[name]
|
||||
|
||||
cacheReadsCounter.With(prometheus.Labels{
|
||||
"hit": strconv.FormatBool(exists),
|
||||
"method": "byName",
|
||||
}).Inc()
|
||||
|
||||
if !exists || entry.expired() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (c *dataKeyCache) add(entry *dataKeyCacheEntry) {
|
||||
c.mtx.Lock()
|
||||
defer c.mtx.Unlock()
|
||||
|
||||
entry.expiration = now().Add(c.cacheTTL)
|
||||
|
||||
c.byId[entry.id] = entry
|
||||
c.byName[entry.name] = entry
|
||||
}
|
||||
|
||||
func (c *dataKeyCache) removeExpired() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.mtx.Lock()
|
||||
defer c.mtx.Unlock()
|
||||
|
||||
for id, entry := range c.entries {
|
||||
for id, entry := range c.byId {
|
||||
if entry.expired() {
|
||||
delete(c.entries, id)
|
||||
delete(c.byId, id)
|
||||
}
|
||||
}
|
||||
|
||||
for name, entry := range c.byName {
|
||||
if entry.expired() {
|
||||
delete(c.byName, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *dataKeyCache) flush() {
|
||||
c.Lock()
|
||||
c.entries = make(map[string]dataKeyCacheEntry)
|
||||
c.Unlock()
|
||||
c.mtx.Lock()
|
||||
c.byId = make(map[string]*dataKeyCacheEntry)
|
||||
c.byName = make(map[string]*dataKeyCacheEntry)
|
||||
c.mtx.Unlock()
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/kmsproviders"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"xorm.io/xorm"
|
||||
@@ -29,10 +31,13 @@ type SecretsService struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
usageStats usagestats.Service
|
||||
|
||||
currentProviderID secrets.ProviderID
|
||||
mtx sync.Mutex
|
||||
dataKeyCache *dataKeyCache
|
||||
|
||||
providers map[secrets.ProviderID]secrets.Provider
|
||||
dataKeyCache *dataKeyCache
|
||||
log log.Logger
|
||||
currentProviderID secrets.ProviderID
|
||||
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func ProvideSecretsService(
|
||||
@@ -62,10 +67,9 @@ func ProvideSecretsService(
|
||||
logger.Warn("Changing encryption provider requires enabling envelope encryption feature")
|
||||
}
|
||||
|
||||
logger.Debug("Envelope encryption state", "enabled", enabled, "current provider", currentProviderID)
|
||||
logger.Info("Envelope encryption state", "enabled", enabled, "current provider", currentProviderID)
|
||||
|
||||
ttl := settings.KeyValue("security.encryption", "data_keys_cache_ttl").MustDuration(15 * time.Minute)
|
||||
cache := newDataKeyCache(ttl)
|
||||
|
||||
s := &SecretsService{
|
||||
store: store,
|
||||
@@ -73,8 +77,8 @@ func ProvideSecretsService(
|
||||
settings: settings,
|
||||
usageStats: usageStats,
|
||||
providers: providers,
|
||||
dataKeyCache: newDataKeyCache(ttl),
|
||||
currentProviderID: currentProviderID,
|
||||
dataKeyCache: cache,
|
||||
features: features,
|
||||
log: logger,
|
||||
}
|
||||
@@ -142,29 +146,23 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt
|
||||
|
||||
// If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
|
||||
scope := opt()
|
||||
keyName := s.keyName(scope)
|
||||
name := secrets.KeyName(scope, s.currentProviderID)
|
||||
|
||||
var dataKey []byte
|
||||
dataKey, err = s.dataKey(ctx, keyName)
|
||||
id, dataKey, err := s.currentDataKey(ctx, name, scope, sess)
|
||||
if err != nil {
|
||||
if errors.Is(err, secrets.ErrDataKeyNotFound) {
|
||||
dataKey, err = s.newDataKey(ctx, keyName, scope, sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
s.log.Error("Failed to get current data key", "error", err, "name", name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var encrypted []byte
|
||||
encrypted, err = s.enc.Encrypt(ctx, payload, string(dataKey))
|
||||
if err != nil {
|
||||
s.log.Error("Failed to encrypt secret", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefix := make([]byte, b64.EncodedLen(len(keyName))+2)
|
||||
b64.Encode(prefix[1:], []byte(keyName))
|
||||
prefix := make([]byte, b64.EncodedLen(len(id))+2)
|
||||
b64.Encode(prefix[1:], []byte(id))
|
||||
prefix[0] = '#'
|
||||
prefix[len(prefix)-1] = '#'
|
||||
|
||||
@@ -175,8 +173,121 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
func (s *SecretsService) keyName(scope string) string {
|
||||
return fmt.Sprintf("%s/%s@%s", now().Format("2006-01-02"), scope, s.currentProviderID)
|
||||
// currentDataKey looks up for current data key in cache or database by name, and decrypts it.
|
||||
// If there's no current data key in cache nor in database it generates a new random data key,
|
||||
// and stores it into both the in-memory cache and database (encrypted by the encryption provider).
|
||||
func (s *SecretsService) currentDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) (string, []byte, error) {
|
||||
// We want only one request fetching current data key at time to
|
||||
// avoid the creation of multiple ones in case there's no one existing.
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
// We try to fetch the data key, either from cache or database
|
||||
id, dataKey, err := s.dataKeyByName(ctx, name)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// If no existing data key was found, create a new one
|
||||
if dataKey == nil {
|
||||
id, dataKey, err = s.newDataKey(ctx, name, scope, sess)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return id, dataKey, nil
|
||||
}
|
||||
|
||||
// dataKeyByName looks up for data key in cache.
|
||||
// Otherwise, it fetches it from database, decrypts it and caches it decrypted.
|
||||
func (s *SecretsService) dataKeyByName(ctx context.Context, name string) (string, []byte, error) {
|
||||
// 0. Get data key from in-memory cache.
|
||||
if entry, exists := s.dataKeyCache.getByName(name); exists {
|
||||
return entry.id, entry.dataKey, nil
|
||||
}
|
||||
|
||||
// 1. Get data key from database.
|
||||
dataKey, err := s.store.GetCurrentDataKey(ctx, name)
|
||||
if err != nil {
|
||||
if errors.Is(err, secrets.ErrDataKeyNotFound) {
|
||||
return "", nil, nil
|
||||
}
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 2.1 Find the encryption provider.
|
||||
provider, exists := s.providers[kmsproviders.NormalizeProviderID(dataKey.Provider)]
|
||||
if !exists {
|
||||
return "", nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider)
|
||||
}
|
||||
|
||||
// 2.2 Decrypt the data key fetched from the database.
|
||||
decrypted, err := provider.Decrypt(ctx, dataKey.EncryptedData)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 3. Store the decrypted data key into the in-memory cache.
|
||||
s.dataKeyCache.add(&dataKeyCacheEntry{id: dataKey.Id, name: dataKey.Name, dataKey: decrypted})
|
||||
|
||||
return dataKey.Id, decrypted, nil
|
||||
}
|
||||
|
||||
// newDataKey creates a new random data key, encrypts it and stores it into the database and cache.
|
||||
func (s *SecretsService) newDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) (string, []byte, error) {
|
||||
// 1. Create new data key.
|
||||
dataKey, err := newRandomDataKey()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 2.1 Find the encryption provider.
|
||||
provider, exists := s.providers[s.currentProviderID]
|
||||
if !exists {
|
||||
return "", nil, fmt.Errorf("could not find encryption provider '%s'", s.currentProviderID)
|
||||
}
|
||||
|
||||
// 2.2 Encrypt the data key.
|
||||
encrypted, err := provider.Encrypt(ctx, dataKey)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 3. Store its encrypted value into the DB.
|
||||
id := util.GenerateShortUID()
|
||||
dbDataKey := secrets.DataKey{
|
||||
Id: id,
|
||||
Active: true,
|
||||
Name: name,
|
||||
Provider: s.currentProviderID,
|
||||
EncryptedData: encrypted,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
if sess == nil {
|
||||
err = s.store.CreateDataKey(ctx, &dbDataKey)
|
||||
} else {
|
||||
err = s.store.CreateDataKeyWithDBSession(ctx, &dbDataKey, sess)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 4. Store the decrypted data key into the in-memory cache.
|
||||
s.dataKeyCache.add(&dataKeyCacheEntry{id: id, name: name, dataKey: dataKey})
|
||||
|
||||
return id, dataKey, nil
|
||||
}
|
||||
|
||||
func newRandomDataKey() ([]byte, error) {
|
||||
rawDataKey := make([]byte, 16)
|
||||
_, err := rand.Read(rawDataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rawDataKey, nil
|
||||
}
|
||||
|
||||
func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
@@ -192,6 +303,10 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e
|
||||
"success": strconv.FormatBool(err == nil),
|
||||
"operation": OpDecrypt,
|
||||
}).Inc()
|
||||
|
||||
if err != nil {
|
||||
s.log.Error("Failed to decrypt secret", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(payload) == 0 {
|
||||
@@ -208,20 +323,20 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e
|
||||
payload = payload[1:]
|
||||
endOfKey := bytes.Index(payload, []byte{'#'})
|
||||
if endOfKey == -1 {
|
||||
err = fmt.Errorf("could not find valid key in encrypted payload")
|
||||
err = fmt.Errorf("could not find valid key id in encrypted payload")
|
||||
return nil, err
|
||||
}
|
||||
b64Key := payload[:endOfKey]
|
||||
payload = payload[endOfKey+1:]
|
||||
key := make([]byte, b64.DecodedLen(len(b64Key)))
|
||||
_, err = b64.Decode(key, b64Key)
|
||||
keyId := make([]byte, b64.DecodedLen(len(b64Key)))
|
||||
_, err = b64.Decode(keyId, b64Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataKey, err = s.dataKey(ctx, string(key))
|
||||
dataKey, err = s.dataKeyById(ctx, string(keyId))
|
||||
if err != nil {
|
||||
s.log.Error("Failed to lookup data key", "name", string(key), "error", err)
|
||||
s.log.Error("Failed to lookup data key by id", "id", string(keyId), "error", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -275,83 +390,34 @@ func (s *SecretsService) GetDecryptedValue(ctx context.Context, sjd map[string][
|
||||
return fallback
|
||||
}
|
||||
|
||||
func newRandomDataKey() ([]byte, error) {
|
||||
rawDataKey := make([]byte, 16)
|
||||
_, err := rand.Read(rawDataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rawDataKey, nil
|
||||
}
|
||||
|
||||
// newDataKey creates a new random DEK, caches it and returns its value
|
||||
func (s *SecretsService) newDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) ([]byte, error) {
|
||||
// 1. Create new DEK
|
||||
dataKey, err := newRandomDataKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider, exists := s.providers[s.currentProviderID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("could not find encryption provider '%s'", s.currentProviderID)
|
||||
// dataKeyById looks up for data key in cache.
|
||||
// Otherwise, it fetches it from database and returns it decrypted.
|
||||
func (s *SecretsService) dataKeyById(ctx context.Context, id string) ([]byte, error) {
|
||||
// 0. Get decrypted data key from in-memory cache.
|
||||
if entry, exists := s.dataKeyCache.getById(id); exists {
|
||||
return entry.dataKey, nil
|
||||
}
|
||||
|
||||
// 2. Encrypt it
|
||||
encrypted, err := provider.Encrypt(ctx, dataKey)
|
||||
// 1. Get encrypted data key from database.
|
||||
dataKey, err := s.store.GetDataKey(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. Store its encrypted value in db
|
||||
dek := secrets.DataKey{
|
||||
Active: true, // TODO: right now we never mark a key as deactivated
|
||||
Name: name,
|
||||
Provider: s.currentProviderID,
|
||||
EncryptedData: encrypted,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
if sess == nil {
|
||||
err = s.store.CreateDataKey(ctx, dek)
|
||||
} else {
|
||||
err = s.store.CreateDataKeyWithDBSession(ctx, dek, sess)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. Cache its unencrypted value and return it
|
||||
s.dataKeyCache.add(name, dataKey)
|
||||
|
||||
return dataKey, nil
|
||||
}
|
||||
|
||||
// dataKey looks up DEK in cache or database, and decrypts it
|
||||
func (s *SecretsService) dataKey(ctx context.Context, name string) ([]byte, error) {
|
||||
if dataKey, exists := s.dataKeyCache.get(name); exists {
|
||||
return dataKey, nil
|
||||
}
|
||||
|
||||
// 1. get encrypted data key from database
|
||||
dataKey, err := s.store.GetDataKey(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. decrypt data key
|
||||
// 2.1. Find the encryption provider.
|
||||
provider, exists := s.providers[kmsproviders.NormalizeProviderID(dataKey.Provider)]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider)
|
||||
}
|
||||
|
||||
// 2.2. Encrypt the data key.
|
||||
decrypted, err := provider.Decrypt(ctx, dataKey.EncryptedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. cache data key
|
||||
s.dataKeyCache.add(name, decrypted)
|
||||
// 3. Store the decrypted data key into the in-memory cache.
|
||||
s.dataKeyCache.add(&dataKeyCacheEntry{id: id, name: dataKey.Name, dataKey: decrypted})
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
@@ -360,17 +426,38 @@ func (s *SecretsService) GetProviders() map[secrets.ProviderID]secrets.Provider
|
||||
return s.providers
|
||||
}
|
||||
|
||||
func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error {
|
||||
err := s.store.ReEncryptDataKeys(ctx, s.providers, s.currentProviderID)
|
||||
func (s *SecretsService) RotateDataKeys(ctx context.Context) error {
|
||||
s.log.Info("Data keys rotation triggered, acquiring lock...")
|
||||
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
s.log.Info("Data keys rotation started")
|
||||
err := s.store.DisableDataKeys(ctx)
|
||||
if err != nil {
|
||||
s.log.Error("Data keys rotation failed", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.dataKeyCache.flush()
|
||||
s.log.Info("Data keys rotation finished successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error {
|
||||
s.log.Info("Data keys re-encryption triggered")
|
||||
err := s.store.ReEncryptDataKeys(ctx, s.providers, s.currentProviderID)
|
||||
if err != nil {
|
||||
s.log.Error("Data keys re-encryption failed", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.dataKeyCache.flush()
|
||||
s.log.Info("Data keys re-encryption finished successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SecretsService) Run(ctx context.Context) error {
|
||||
gc := time.NewTicker(
|
||||
s.settings.KeyValue("security.encryption", "data_keys_cache_cleanup_interval").
|
||||
@@ -390,11 +477,11 @@ func (s *SecretsService) Run(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-gc.C:
|
||||
s.log.Debug("removing expired data encryption keys from cache...")
|
||||
s.log.Debug("Removing expired data keys from cache...")
|
||||
s.dataKeyCache.removeExpired()
|
||||
s.log.Debug("done removing expired data encryption keys from cache")
|
||||
s.log.Debug("Removing expired data keys from cache finished successfully")
|
||||
case <-gCtx.Done():
|
||||
s.log.Debug("grafana is shutting down; stopping...")
|
||||
s.log.Debug("Grafana is shutting down; stopping...")
|
||||
gc.Stop()
|
||||
|
||||
if err := grp.Wait(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
|
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/secrets/database"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
@@ -97,7 +98,8 @@ func TestSecretsService_DataKeys(t *testing.T) {
|
||||
store := database.ProvideSecretsStore(sqlstore.InitTestDB(t))
|
||||
ctx := context.Background()
|
||||
|
||||
dataKey := secrets.DataKey{
|
||||
dataKey := &secrets.DataKey{
|
||||
Id: util.GenerateShortUID(),
|
||||
Active: true,
|
||||
Name: "test1",
|
||||
Provider: "test",
|
||||
@@ -105,7 +107,7 @@ func TestSecretsService_DataKeys(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("querying for a DEK that does not exist", func(t *testing.T) {
|
||||
res, err := store.GetDataKey(ctx, dataKey.Name)
|
||||
res, err := store.GetDataKey(ctx, dataKey.Id)
|
||||
assert.ErrorIs(t, secrets.ErrDataKeyNotFound, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
@@ -114,16 +116,26 @@ func TestSecretsService_DataKeys(t *testing.T) {
|
||||
err := store.CreateDataKey(ctx, dataKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := store.GetDataKey(ctx, dataKey.Name)
|
||||
res, err := store.GetDataKey(ctx, dataKey.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dataKey.EncryptedData, res.EncryptedData)
|
||||
assert.Equal(t, dataKey.Provider, res.Provider)
|
||||
assert.Equal(t, dataKey.Name, res.Name)
|
||||
assert.Equal(t, dataKey.Id, res.Id)
|
||||
assert.True(t, dataKey.Active)
|
||||
|
||||
current, err := store.GetCurrentDataKey(ctx, dataKey.Name)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dataKey.EncryptedData, current.EncryptedData)
|
||||
assert.Equal(t, dataKey.Provider, current.Provider)
|
||||
assert.Equal(t, dataKey.Name, current.Name)
|
||||
assert.Equal(t, dataKey.Id, current.Id)
|
||||
assert.True(t, current.Active)
|
||||
})
|
||||
|
||||
t.Run("creating an inactive DEK", func(t *testing.T) {
|
||||
k := secrets.DataKey{
|
||||
k := &secrets.DataKey{
|
||||
Id: util.GenerateShortUID(),
|
||||
Active: false,
|
||||
Name: "test2",
|
||||
Provider: "test",
|
||||
@@ -138,7 +150,7 @@ func TestSecretsService_DataKeys(t *testing.T) {
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
|
||||
t.Run("deleting DEK when no name provided must fail", func(t *testing.T) {
|
||||
t.Run("deleting DEK when no id provided must fail", func(t *testing.T) {
|
||||
beforeDelete, err := store.GetAllDataKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
err = store.DeleteDataKey(ctx, "")
|
||||
@@ -150,10 +162,10 @@ func TestSecretsService_DataKeys(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("deleting a DEK", func(t *testing.T) {
|
||||
err := store.DeleteDataKey(ctx, dataKey.Name)
|
||||
err := store.DeleteDataKey(ctx, dataKey.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := store.GetDataKey(ctx, dataKey.Name)
|
||||
res, err := store.GetDataKey(ctx, dataKey.Id)
|
||||
assert.Equal(t, secrets.ErrDataKeyNotFound, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
@@ -280,7 +292,8 @@ func TestSecretsService_Run(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Data encryption key cache should contain one element
|
||||
require.Len(t, svc.dataKeyCache.entries, 1)
|
||||
require.Len(t, svc.dataKeyCache.byId, 1)
|
||||
require.Len(t, svc.dataKeyCache.byName, 1)
|
||||
|
||||
t.Cleanup(func() { now = time.Now })
|
||||
now = func() time.Time { return time.Now().Add(10 * time.Minute) }
|
||||
@@ -294,7 +307,8 @@ func TestSecretsService_Run(t *testing.T) {
|
||||
// Then, once the ticker has been triggered,
|
||||
// the cleanup process should have happened,
|
||||
// therefore the cache should be empty.
|
||||
require.Len(t, svc.dataKeyCache.entries, 0)
|
||||
require.Len(t, svc.dataKeyCache.byId, 0)
|
||||
require.Len(t, svc.dataKeyCache.byName, 0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -328,11 +342,13 @@ func TestSecretsService_ReEncryptDataKeys(t *testing.T) {
|
||||
// Decrypt to ensure data key is cached
|
||||
_, err := svc.Decrypt(ctx, ciphertext)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, svc.dataKeyCache.entries)
|
||||
require.NotEmpty(t, svc.dataKeyCache.byId)
|
||||
require.NotEmpty(t, svc.dataKeyCache.byName)
|
||||
|
||||
err = svc.ReEncryptDataKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, svc.dataKeyCache.entries)
|
||||
assert.Empty(t, svc.dataKeyCache.byId)
|
||||
assert.Empty(t, svc.dataKeyCache.byName)
|
||||
})
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ var (
|
||||
Name: "encryption_cache_reads_total",
|
||||
Help: "A counter for encryption cache reads",
|
||||
},
|
||||
[]string{"hit"},
|
||||
[]string{"hit", "method"},
|
||||
)
|
||||
)
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@@ -25,16 +26,19 @@ type Service interface {
|
||||
|
||||
GetDecryptedValue(ctx context.Context, sjd map[string][]byte, key, fallback string) string
|
||||
|
||||
RotateDataKeys(ctx context.Context) error
|
||||
ReEncryptDataKeys(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Store defines methods to interact with secrets storage
|
||||
type Store interface {
|
||||
GetDataKey(ctx context.Context, name string) (*DataKey, error)
|
||||
GetDataKey(ctx context.Context, id string) (*DataKey, error)
|
||||
GetCurrentDataKey(ctx context.Context, name string) (*DataKey, error)
|
||||
GetAllDataKeys(ctx context.Context) ([]*DataKey, error)
|
||||
CreateDataKey(ctx context.Context, dataKey DataKey) error
|
||||
CreateDataKeyWithDBSession(ctx context.Context, dataKey DataKey, sess *xorm.Session) error
|
||||
DeleteDataKey(ctx context.Context, name string) error
|
||||
CreateDataKey(ctx context.Context, dataKey *DataKey) error
|
||||
CreateDataKeyWithDBSession(ctx context.Context, dataKey *DataKey, sess *xorm.Session) error
|
||||
DisableDataKeys(ctx context.Context) error
|
||||
DeleteDataKey(ctx context.Context, id string) error
|
||||
ReEncryptDataKeys(ctx context.Context, providers map[ProviderID]Provider, currProvider ProviderID) error
|
||||
}
|
||||
|
||||
@@ -57,6 +61,10 @@ func (id ProviderID) Kind() (string, error) {
|
||||
return parts[0], nil
|
||||
}
|
||||
|
||||
func KeyName(scope string, providerID ProviderID) string {
|
||||
return fmt.Sprintf("%s/%s@%s", time.Now().Format("2006-01-02"), scope, providerID)
|
||||
}
|
||||
|
||||
// BackgroundProvider should be implemented for a provider that has a task that needs to be run in the background.
|
||||
type BackgroundProvider interface {
|
||||
Run(ctx context.Context) error
|
||||
|
@@ -9,6 +9,7 @@ var ErrDataKeyNotFound = errors.New("data key not found")
|
||||
|
||||
type DataKey struct {
|
||||
Active bool
|
||||
Id string
|
||||
Name string
|
||||
Scope string
|
||||
Provider ProviderID
|
||||
|
@@ -1,6 +1,10 @@
|
||||
package migrations
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
func addSecretsMigration(mg *migrator.Migrator) {
|
||||
dataKeysV1 := migrator.Table{
|
||||
@@ -38,4 +42,23 @@ func addSecretsMigration(mg *migrator.Migrator) {
|
||||
}
|
||||
|
||||
mg.AddMigration("create secrets table", migrator.NewAddTableMigration(secretsV1))
|
||||
|
||||
mg.AddMigration("rename data_keys name column to id", migrator.NewRenameColumnMigration(
|
||||
dataKeysV1, "name", "id",
|
||||
))
|
||||
|
||||
mg.AddMigration("add name column into data_keys", migrator.NewAddColumnMigration(
|
||||
dataKeysV1,
|
||||
&migrator.Column{
|
||||
Name: "name",
|
||||
Type: migrator.DB_NVarchar,
|
||||
Length: 100,
|
||||
Default: "''",
|
||||
Nullable: false,
|
||||
},
|
||||
))
|
||||
|
||||
mg.AddMigration("copy data_keys id column values into name", migrator.NewRawSQLMigration(
|
||||
fmt.Sprintf("UPDATE %s SET %s = %s", dataKeysV1.Name, "name", "id"),
|
||||
))
|
||||
}
|
||||
|
@@ -37,6 +37,8 @@ type Dialect interface {
|
||||
DropIndexSQL(tableName string, index *Index) string
|
||||
|
||||
RenameTable(oldName string, newName string) string
|
||||
RenameColumn(table Table, oldName, newName string) string
|
||||
|
||||
UpdateTableSQL(tableName string, columns []*Column) string
|
||||
|
||||
IndexCheckSQL(tableName, indexName string) (string, []interface{})
|
||||
@@ -209,6 +211,11 @@ func (b *BaseDialect) RenameTable(oldName string, newName string) string {
|
||||
return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", quote(oldName), quote(newName))
|
||||
}
|
||||
|
||||
func (b *BaseDialect) RenameColumn(table Table, oldName, newName string) string {
|
||||
quote := b.dialect.Quote
|
||||
return fmt.Sprintf("ALTER TABLE %s RENAME COLUMN %s TO %s", quote(table.Name), quote(oldName), quote(newName))
|
||||
}
|
||||
|
||||
func (b *BaseDialect) ColumnCheckSQL(tableName, columnName string) (string, []interface{}) {
|
||||
return "", nil
|
||||
}
|
||||
|
@@ -108,6 +108,32 @@ func (m *AddColumnMigration) SQL(dialect Dialect) string {
|
||||
return dialect.AddColumnSQL(m.tableName, m.column)
|
||||
}
|
||||
|
||||
type RenameColumnMigration struct {
|
||||
MigrationBase
|
||||
table Table
|
||||
oldName string
|
||||
newName string
|
||||
}
|
||||
|
||||
func NewRenameColumnMigration(table Table, oldName, newName string) *RenameColumnMigration {
|
||||
return &RenameColumnMigration{table: table, oldName: oldName, newName: newName}
|
||||
}
|
||||
|
||||
func (m *RenameColumnMigration) Table(table Table) *RenameColumnMigration {
|
||||
m.table = table
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *RenameColumnMigration) Rename(oldName string, newName string) *RenameColumnMigration {
|
||||
m.oldName = oldName
|
||||
m.newName = newName
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *RenameColumnMigration) SQL(d Dialect) string {
|
||||
return d.RenameColumn(m.table, m.oldName, m.newName)
|
||||
}
|
||||
|
||||
type AddIndexMigration struct {
|
||||
MigrationBase
|
||||
tableName string
|
||||
|
@@ -118,6 +118,20 @@ func (db *MySQLDialect) ColumnCheckSQL(tableName, columnName string) (string, []
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func (db *MySQLDialect) RenameColumn(table Table, oldName, newName string) string {
|
||||
var colType string
|
||||
for _, col := range table.Columns {
|
||||
if col.Name == oldName {
|
||||
colType = db.SQLType(col)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
quote := db.dialect.Quote
|
||||
|
||||
return fmt.Sprintf("ALTER TABLE %s CHANGE %s %s %s", quote(table.Name), quote(oldName), quote(newName), colType)
|
||||
}
|
||||
|
||||
func (db *MySQLDialect) CleanDB() error {
|
||||
tables, err := db.engine.DBMetas()
|
||||
if err != nil {
|
||||
|
@@ -103,6 +103,7 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS
|
||||
sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_panels,`, models.PanelElement)
|
||||
sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_variables,`, models.VariableElement)
|
||||
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_keys") + `) AS data_keys,`)
|
||||
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_keys") + `WHERE active = true) AS active_data_keys,`)
|
||||
|
||||
sb.Write(ss.roleCounterSQL(ctx))
|
||||
|
||||
|
Reference in New Issue
Block a user