grafana/pkg/services/secrets/kvstore/sql.go
Kristin Laemmert 05709ce411
chore: remove sqlstore & mockstore dependencies from (most) packages (#57087)
* chore: add alias for InitTestDB and Session

Adds an alias for the sqlstore InitTestDB and Session, and updates tests using these to reduce dependencies on the sqlstore.Store.

* next pass of removing sqlstore imports
* last little bit
* remove mockstore where possible
2022-10-19 09:02:15 -04:00

270 lines
7.8 KiB
Go

package kvstore
import (
"context"
"encoding/base64"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/secrets"
)
// SecretsKVStoreSQL provides a key/value store backed by the Grafana database
type SecretsKVStoreSQL struct {
log log.Logger
sqlStore db.DB
secretsService secrets.Service
decryptionCache decryptionCache
}
type decryptionCache struct {
cache map[int64]cachedDecrypted
sync.Mutex
}
type cachedDecrypted struct {
updated time.Time
value string
}
var b64 = base64.RawStdEncoding
func NewSQLSecretsKVStore(sqlStore db.DB, secretsService secrets.Service, logger log.Logger) *SecretsKVStoreSQL {
return &SecretsKVStoreSQL{
sqlStore: sqlStore,
secretsService: secretsService,
log: logger,
decryptionCache: decryptionCache{
cache: make(map[int64]cachedDecrypted),
},
}
}
// 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 *db.Session) 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.Debug("secret value not found", "orgId", orgId, "type", typ, "namespace", namespace)
return nil
}
isFound = true
return nil
})
if err == nil && isFound {
decryptedValue, err = kv.getDecryptedValue(ctx, item)
if err != nil {
kv.log.Error("error decrypting secret value", "orgId", item.OrgId, "type", item.Type, "namespace", item.Namespace, "err", err)
return string(decryptedValue), isFound, err
}
}
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 *db.Session) 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 *db.Session) 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 *db.Session) 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 *db.Session) 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) {
var items []Item
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
return dbSession.Find(&items)
})
if err != nil {
kv.log.Error("error getting all the items", "err", err)
return nil, err
}
// decrypting values
for i := range items {
value, err := kv.getDecryptedValue(ctx, items[i])
items[i].Value = string(value)
if err != nil {
kv.log.Error("error decrypting secret value", "orgId", items[i].OrgId, "type", items[i].Type, "namespace", items[i].Namespace, "err", err)
}
}
return items, err
}
func (kv *SecretsKVStoreSQL) getDecryptedValue(ctx context.Context, item Item) ([]byte, error) {
kv.decryptionCache.Lock()
defer kv.decryptionCache.Unlock()
var decryptedValue []byte
var err error
if cache, ok := kv.decryptionCache.cache[item.Id]; ok && item.Updated.Equal(cache.updated) {
return []byte(cache.value), err
}
decodedValue, err := b64.DecodeString(item.Value)
if err != nil {
return decryptedValue, err
}
decryptedValue, err = kv.secretsService.Decrypt(ctx, decodedValue)
if err != nil {
return decryptedValue, err
}
kv.decryptionCache.cache[item.Id] = cachedDecrypted{
updated: item.Updated,
value: string(decryptedValue),
}
return decryptedValue, err
}