mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* add special handling on the plugin gathering side to check whether secrets manager plugins are enabled or not * show disabled badge in front end if the plugin is not enabled * Only show error in disabled badge hover if one is present (otherwise it shows "undefined") * refactor to make use of fields already available in the DTO * fix typo * if there is no error returned for the plugin, just show 'disabled' * fix typo * Update public/app/features/plugins/admin/components/Badges/PluginDisabledBadge.tsx Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com> * Update frontendsettings.go add clarifying comment * fix unit test * rework task to use new frontend property combined with plugin type to determine if the plugin should be disabled * Update helpers.test.ts revert test change * fix unit test * show custom uninstall message if the plugin is a secrets manager * bogus commit to trigger precommit * undo commit * run precommit manually * add some consts * refactor a bit to pull plugin error management up a level * re-add code squashed in merge * fix compile issues * add code to set plugin error fatal flag after secret migration * refactor to move plugin startup out of Should Check func * re-add important check * make plugin startup errors fatal the first time we set a secret on the plugin * rename func to make intent clearler * remove unnecessary duplicate code from plugin mig * fix compile error * fix more compile errors * add some extra logging to secrets migration * have remote_plugin secret service managed plugin error fatal flag directly * add blank file for eventual unit tests * fix linting issues * changes from PR review * quick bit of cleanup * add comment explaining design decision * move more common test helpers to file * slightly update to first time Get secret call * add unit tests * remove override func from provider * fix linting issues * add test cleanup step * add some comments about refactoring to hacky test function Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
278 lines
8.6 KiB
Go
278 lines
8.6 KiB
Go
package kvstore
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/services/secrets"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
)
|
|
|
|
// secretsKVStoreSQL provides a key/value store backed by the Grafana database
|
|
type secretsKVStoreSQL struct {
|
|
log log.Logger
|
|
sqlStore sqlstore.Store
|
|
secretsService secrets.Service
|
|
decryptionCache decryptionCache
|
|
// This is here to support testing and should normally not be set
|
|
GetAllFuncOverride func(ctx context.Context) ([]Item, error)
|
|
}
|
|
|
|
type decryptionCache struct {
|
|
cache map[int64]cachedDecrypted
|
|
sync.Mutex
|
|
}
|
|
|
|
type cachedDecrypted struct {
|
|
updated time.Time
|
|
value string
|
|
}
|
|
|
|
var b64 = base64.RawStdEncoding
|
|
|
|
// Get an item from the store
|
|
func (kv *secretsKVStoreSQL) Get(ctx context.Context, orgId int64, namespace string, typ string) (string, bool, error) {
|
|
item := Item{
|
|
OrgId: &orgId,
|
|
Namespace: &namespace,
|
|
Type: &typ,
|
|
}
|
|
var isFound bool
|
|
var decryptedValue []byte
|
|
|
|
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
|
has, err := dbSession.Get(&item)
|
|
if err != nil {
|
|
kv.log.Error("error getting secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
return err
|
|
}
|
|
if !has {
|
|
kv.log.Error("secret value not found", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
return nil
|
|
}
|
|
isFound = true
|
|
return nil
|
|
})
|
|
|
|
if err == nil && isFound {
|
|
kv.decryptionCache.Lock()
|
|
defer kv.decryptionCache.Unlock()
|
|
|
|
if cache, ok := kv.decryptionCache.cache[item.Id]; ok && item.Updated.Equal(cache.updated) {
|
|
kv.log.Debug("got secret value from decryption cache", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
return cache.value, isFound, err
|
|
}
|
|
|
|
decodedValue, err := b64.DecodeString(item.Value)
|
|
if err != nil {
|
|
kv.log.Error("error decoding secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
return string(decryptedValue), isFound, err
|
|
}
|
|
|
|
decryptedValue, err = kv.secretsService.Decrypt(ctx, decodedValue)
|
|
if err != nil {
|
|
kv.log.Error("error decrypting secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
return string(decryptedValue), isFound, err
|
|
}
|
|
|
|
kv.decryptionCache.cache[item.Id] = cachedDecrypted{
|
|
updated: item.Updated,
|
|
value: string(decryptedValue),
|
|
}
|
|
}
|
|
|
|
kv.log.Debug("got secret value", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
return string(decryptedValue), isFound, err
|
|
}
|
|
|
|
// Set an item in the store
|
|
func (kv *secretsKVStoreSQL) Set(ctx context.Context, orgId int64, namespace string, typ string, value string) error {
|
|
encryptedValue, err := kv.secretsService.Encrypt(ctx, []byte(value), secrets.WithoutScope())
|
|
if err != nil {
|
|
kv.log.Error("error encrypting secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
return err
|
|
}
|
|
encodedValue := b64.EncodeToString(encryptedValue)
|
|
return kv.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
|
item := Item{
|
|
OrgId: &orgId,
|
|
Namespace: &namespace,
|
|
Type: &typ,
|
|
}
|
|
|
|
has, err := dbSession.Get(&item)
|
|
if err != nil {
|
|
kv.log.Error("error checking secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
return err
|
|
}
|
|
|
|
if has && item.Value == encodedValue {
|
|
kv.log.Debug("secret value not changed", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
return nil
|
|
}
|
|
|
|
item.Value = encodedValue
|
|
item.Updated = time.Now()
|
|
|
|
if has {
|
|
// if item already exists we update it
|
|
_, err = dbSession.ID(item.Id).Update(&item)
|
|
if err != nil {
|
|
kv.log.Error("error updating secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
} else {
|
|
kv.decryptionCache.Lock()
|
|
defer kv.decryptionCache.Unlock()
|
|
kv.decryptionCache.cache[item.Id] = cachedDecrypted{
|
|
updated: item.Updated,
|
|
value: value,
|
|
}
|
|
kv.log.Debug("secret value updated", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// if item doesn't exist we create it
|
|
item.Created = item.Updated
|
|
_, err = dbSession.Insert(&item)
|
|
if err != nil {
|
|
kv.log.Error("error inserting secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
} else {
|
|
kv.log.Debug("secret value inserted", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Del deletes an item from the store.
|
|
func (kv *secretsKVStoreSQL) Del(ctx context.Context, orgId int64, namespace string, typ string) error {
|
|
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
|
item := Item{
|
|
OrgId: &orgId,
|
|
Namespace: &namespace,
|
|
Type: &typ,
|
|
}
|
|
|
|
has, err := dbSession.Get(&item)
|
|
if err != nil {
|
|
kv.log.Error("error checking secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
return err
|
|
}
|
|
|
|
if has {
|
|
// if item exists we delete it
|
|
_, err = dbSession.ID(item.Id).Delete(&item)
|
|
if err != nil {
|
|
kv.log.Error("error deleting secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
} else {
|
|
kv.decryptionCache.Lock()
|
|
defer kv.decryptionCache.Unlock()
|
|
delete(kv.decryptionCache.cache, item.Id)
|
|
kv.log.Debug("secret value deleted", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
return err
|
|
}
|
|
|
|
// Keys get all keys for a given namespace. To query for all
|
|
// organizations the constant 'kvstore.AllOrganizations' can be passed as orgId.
|
|
func (kv *secretsKVStoreSQL) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) {
|
|
var keys []Key
|
|
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
|
query := dbSession.Where("namespace = ?", namespace).And("type = ?", typ)
|
|
if orgId != AllOrganizations {
|
|
query.And("org_id = ?", orgId)
|
|
}
|
|
return query.Find(&keys)
|
|
})
|
|
return keys, err
|
|
}
|
|
|
|
// Rename an item in the store
|
|
func (kv *secretsKVStoreSQL) Rename(ctx context.Context, orgId int64, namespace string, typ string, newNamespace string) error {
|
|
return kv.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
|
item := Item{
|
|
OrgId: &orgId,
|
|
Namespace: &namespace,
|
|
Type: &typ,
|
|
}
|
|
|
|
has, err := dbSession.Get(&item)
|
|
if err != nil {
|
|
kv.log.Error("error checking secret value", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
return err
|
|
}
|
|
|
|
item.Namespace = &newNamespace
|
|
item.Updated = time.Now()
|
|
|
|
if has {
|
|
// if item already exists we update it
|
|
_, err = dbSession.ID(item.Id).Update(&item)
|
|
if err != nil {
|
|
kv.log.Error("error updating secret namespace", "orgId", orgId, "type", typ, "namespace", namespace, "err", err)
|
|
} else {
|
|
kv.log.Debug("secret namespace updated", "orgId", orgId, "type", typ, "namespace", namespace)
|
|
}
|
|
return err
|
|
}
|
|
|
|
return err
|
|
})
|
|
}
|
|
|
|
// GetAll this returns all the secrets stored in the database. This is not part of the kvstore interface as we
|
|
// only need it for migration from sql to plugin at this moment
|
|
func (kv *secretsKVStoreSQL) GetAll(ctx context.Context) ([]Item, error) {
|
|
if kv.GetAllFuncOverride != nil {
|
|
return kv.GetAllFuncOverride(ctx)
|
|
}
|
|
var items []Item
|
|
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
|
return dbSession.Find(&items)
|
|
})
|
|
if err != nil {
|
|
kv.log.Error("error getting all the items", "err", err)
|
|
return nil, err
|
|
}
|
|
|
|
// decrypting value
|
|
kv.decryptionCache.Lock()
|
|
defer kv.decryptionCache.Unlock()
|
|
for i := range items {
|
|
var decryptedValue []byte
|
|
if cache, ok := kv.decryptionCache.cache[items[i].Id]; ok && items[i].Updated.Equal(cache.updated) {
|
|
kv.log.Debug("got secret value from decryption cache", "orgId", items[i].OrgId, "type", items[i].Type, "namespace", items[i].Namespace)
|
|
items[i].Value = cache.value
|
|
continue
|
|
}
|
|
|
|
decodedValue, err := b64.DecodeString(items[i].Value)
|
|
if err != nil {
|
|
kv.log.Error("error decoding secret value", "orgId", items[i].OrgId, "type", items[i].Type, "namespace", items[i].Namespace, "err", err)
|
|
items[i].Value = string(decryptedValue)
|
|
continue
|
|
}
|
|
|
|
decryptedValue, err = kv.secretsService.Decrypt(ctx, decodedValue)
|
|
if err != nil {
|
|
kv.log.Error("error decrypting secret value", "orgId", items[i].OrgId, "type", items[i].Type, "namespace", items[i].Namespace, "err", err)
|
|
items[i].Value = string(decryptedValue)
|
|
continue
|
|
}
|
|
|
|
items[i].Value = string(decryptedValue)
|
|
kv.decryptionCache.cache[items[i].Id] = cachedDecrypted{
|
|
updated: items[i].Updated,
|
|
value: string(decryptedValue),
|
|
}
|
|
}
|
|
|
|
return items, err
|
|
}
|