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 }