diff --git a/conf/defaults.ini b/conf/defaults.ini index cba6bc9bb40..cedead7db8e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -165,6 +165,9 @@ connstr = # prefix prepended to all the keys in the remote cache prefix = +# This enables encryption of values stored in the remote cache +encryption = + #################################### Data proxy ########################### [dataproxy] diff --git a/conf/sample.ini b/conf/sample.ini index 6789bd286a2..30e4e65cdcc 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -172,6 +172,9 @@ # prefix prepended to all the keys in the remote cache ; prefix = +# This enables encryption of values stored in the remote cache +;encryption = + #################################### Data proxy ########################### [dataproxy] diff --git a/pkg/infra/remotecache/database_storage.go b/pkg/infra/remotecache/database_storage.go index 2b63dd6428a..be5be00011d 100644 --- a/pkg/infra/remotecache/database_storage.go +++ b/pkg/infra/remotecache/database_storage.go @@ -14,12 +14,14 @@ const databaseCacheType = "database" type databaseCache struct { SQLStore db.DB + codec codec log log.Logger } -func newDatabaseCache(sqlstore db.DB) *databaseCache { +func newDatabaseCache(sqlstore db.DB, codec codec) *databaseCache { dc := &databaseCache{ SQLStore: sqlstore, + codec: codec, log: log.New("remotecache.database"), } @@ -78,7 +80,7 @@ func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, erro } } - if err = decodeGob(cacheHit.Data, item); err != nil { + if err = dc.codec.Decode(ctx, cacheHit.Data, item); err != nil { return err } @@ -90,7 +92,7 @@ func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, erro func (dc *databaseCache) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { item := &cachedItem{Val: value} - data, err := encodeGob(item) + data, err := dc.codec.Encode(ctx, item) if err != nil { return err } diff --git a/pkg/infra/remotecache/database_storage_test.go b/pkg/infra/remotecache/database_storage_test.go index 87727683180..6a595c5ff7f 100644 --- a/pkg/infra/remotecache/database_storage_test.go +++ b/pkg/infra/remotecache/database_storage_test.go @@ -16,6 +16,7 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, + codec: &gobCodec{}, log: log.New("remotecache.database"), } @@ -64,6 +65,7 @@ func TestSecondSet(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, + codec: &gobCodec{}, log: log.New("remotecache.database"), } diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go index 56b331cfe99..b8c1b4e4823 100644 --- a/pkg/infra/remotecache/memcached_storage.go +++ b/pkg/infra/remotecache/memcached_storage.go @@ -11,12 +11,14 @@ import ( const memcachedCacheType = "memcached" type memcachedStorage struct { - c *memcache.Client + c *memcache.Client + codec codec } -func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage { +func newMemcachedStorage(opts *setting.RemoteCacheOptions, codec codec) *memcachedStorage { return &memcachedStorage{ - c: memcache.New(opts.ConnStr), + c: memcache.New(opts.ConnStr), + codec: codec, } } @@ -31,7 +33,7 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { // Set sets value to given key in the cache. func (s *memcachedStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} - bytes, err := encodeGob(item) + bytes, err := s.codec.Encode(ctx, item) if err != nil { return err } @@ -58,7 +60,7 @@ func (s *memcachedStorage) Get(ctx context.Context, key string) (interface{}, er item := &cachedItem{} - err = decodeGob(memcachedItem.Value, item) + err = s.codec.Decode(ctx, memcachedItem.Value, item) if err != nil { return nil, err } diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go index bc7909b0162..7c76345a5bf 100644 --- a/pkg/infra/remotecache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -15,7 +15,8 @@ import ( const redisCacheType = "redis" type redisStorage struct { - c *redis.Client + c *redis.Client + codec codec } // parseRedisConnStr parses k=v pairs in csv and builds a redis Options object @@ -76,18 +77,18 @@ func parseRedisConnStr(connStr string) (*redis.Options, error) { return options, nil } -func newRedisStorage(opts *setting.RemoteCacheOptions) (*redisStorage, error) { +func newRedisStorage(opts *setting.RemoteCacheOptions, codec codec) (*redisStorage, error) { opt, err := parseRedisConnStr(opts.ConnStr) if err != nil { return nil, err } - return &redisStorage{c: redis.NewClient(opt)}, nil + return &redisStorage{c: redis.NewClient(opt), codec: codec}, nil } // Set sets value to given key in session. func (s *redisStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} - value, err := encodeGob(item) + value, err := s.codec.Encode(ctx, item) if err != nil { return err } @@ -100,7 +101,7 @@ func (s *redisStorage) Get(ctx context.Context, key string) (interface{}, error) v := s.c.Get(ctx, key) item := &cachedItem{} - err := decodeGob([]byte(v.Val()), item) + err := s.codec.Decode(ctx, []byte(v.Val()), item) if err == nil { return item.Val, nil diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go index 8f572b05e1a..dcbd81f1dd5 100644 --- a/pkg/infra/remotecache/remotecache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" glog "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" ) @@ -29,8 +30,14 @@ const ( ServiceName = "RemoteCache" ) -func ProvideService(cfg *setting.Cfg, sqlStore db.DB) (*RemoteCache, error) { - client, err := createClient(cfg.RemoteCacheOptions, sqlStore) +func ProvideService(cfg *setting.Cfg, sqlStore db.DB, secretsService secrets.Service) (*RemoteCache, error) { + var codec codec + if cfg.RemoteCacheOptions.Encryption { + codec = &encryptionCodec{secretsService} + } else { + codec = &gobCodec{} + } + client, err := createClient(cfg.RemoteCacheOptions, sqlStore, codec) if err != nil { return nil, err } @@ -97,14 +104,14 @@ func (ds *RemoteCache) Run(ctx context.Context) error { return ctx.Err() } -func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB) (cache CacheStorage, err error) { +func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB, codec codec) (cache CacheStorage, err error) { switch opts.Name { case redisCacheType: - cache, err = newRedisStorage(opts) + cache, err = newRedisStorage(opts, codec) case memcachedCacheType: - cache = newMemcachedStorage(opts) + cache = newMemcachedStorage(opts, codec) case databaseCacheType: - cache = newDatabaseCache(sqlstore) + cache = newDatabaseCache(sqlstore, codec) default: return nil, ErrInvalidCacheType } @@ -131,17 +138,46 @@ type cachedItem struct { Val interface{} } -func encodeGob(item *cachedItem) ([]byte, error) { +type codec interface { + Encode(context.Context, *cachedItem) ([]byte, error) + Decode(context.Context, []byte, *cachedItem) error +} + +type gobCodec struct{} + +func (c *gobCodec) Encode(_ context.Context, item *cachedItem) ([]byte, error) { buf := bytes.NewBuffer(nil) err := gob.NewEncoder(buf).Encode(item) return buf.Bytes(), err } -func decodeGob(data []byte, out *cachedItem) error { +func (c *gobCodec) Decode(_ context.Context, data []byte, out *cachedItem) error { buf := bytes.NewBuffer(data) return gob.NewDecoder(buf).Decode(&out) } +type encryptionCodec struct { + secretsService secrets.Service +} + +func (c *encryptionCodec) Encode(ctx context.Context, item *cachedItem) ([]byte, error) { + buf := bytes.NewBuffer(nil) + err := gob.NewEncoder(buf).Encode(item) + if err != nil { + return nil, err + } + return c.secretsService.Encrypt(ctx, buf.Bytes(), secrets.WithoutScope()) +} + +func (c *encryptionCodec) Decode(ctx context.Context, data []byte, out *cachedItem) error { + decrypted, err := c.secretsService.Decrypt(ctx, data) + if err != nil { + return err + } + buf := bytes.NewBuffer(decrypted) + return gob.NewDecoder(buf).Decode(&out) +} + type prefixCacheStorage struct { cache CacheStorage prefix string diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index b72291a519d..7e0947c75d7 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" ) @@ -28,7 +29,7 @@ func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore d cfg := &setting.Cfg{ RemoteCacheOptions: opts, } - dc, err := ProvideService(cfg, sqlstore) + dc, err := ProvideService(cfg, sqlstore, fakes.NewFakeSecretsService()) require.Nil(t, err, "Failed to init client for test") return dc @@ -46,7 +47,7 @@ func TestCachedBasedOnConfig(t *testing.T) { } func TestInvalidCacheTypeReturnsError(t *testing.T) { - _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil) + _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil, &gobCodec{}) assert.Equal(t, err, ErrInvalidCacheType) } @@ -95,6 +96,7 @@ func TestCachePrefix(t *testing.T) { cache := &databaseCache{ SQLStore: db, log: log.New("remotecache.database"), + codec: &gobCodec{}, } prefixCache := &prefixCacheStorage{cache: cache, prefix: "test/"} diff --git a/pkg/infra/remotecache/testing.go b/pkg/infra/remotecache/testing.go index 008fbaafced..f42252aa7c1 100644 --- a/pkg/infra/remotecache/testing.go +++ b/pkg/infra/remotecache/testing.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" ) @@ -22,7 +23,7 @@ func NewFakeStore(t *testing.T) *RemoteCache { dc, err := ProvideService(&setting.Cfg{ RemoteCacheOptions: opts, - }, sqlStore) + }, sqlStore, fakes.NewFakeSecretsService()) require.NoError(t, err, "Failed to init remote cache for test") return dc diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 55737a44720..b53d2935273 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" @@ -79,7 +80,7 @@ func getContextHandler(t *testing.T) *ContextHandler { cfg.AuthProxyHeaderName = "X-Killa" cfg.AuthProxyEnabled = true cfg.AuthProxyHeaderProperty = "username" - remoteCacheSvc, err := remotecache.ProvideService(cfg, sqlStore) + remoteCacheSvc, err := remotecache.ProvideService(cfg, sqlStore, fakes.NewFakeSecretsService()) require.NoError(t, err) userAuthTokenSvc := authtest.NewFakeUserAuthTokenService() renderSvc := &fakeRenderService{} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 81ad5fb72bc..f9a6e1f96e1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1121,11 +1121,13 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { dbName := valueAsString(cacheServer, "type", "database") connStr := valueAsString(cacheServer, "connstr", "") prefix := valueAsString(cacheServer, "prefix", "") + encryption := cacheServer.Key("encryption").MustBool(false) cfg.RemoteCacheOptions = &RemoteCacheOptions{ - Name: dbName, - ConnStr: connStr, - Prefix: prefix, + Name: dbName, + ConnStr: connStr, + Prefix: prefix, + Encryption: encryption, } geomapSection := iniFile.Section("geomap") @@ -1159,9 +1161,10 @@ func valueAsString(section *ini.Section, keyName string, defaultValue string) st } type RemoteCacheOptions struct { - Name string - ConnStr string - Prefix string + Name string + ConnStr string + Prefix string + Encryption bool } func (cfg *Cfg) readLDAPConfig() {