diff --git a/pkg/registry/apis/secret/encryption/manager/dek_cache.go b/pkg/registry/apis/secret/encryption/manager/dek_cache.go deleted file mode 100644 index 9bc39794eb5..00000000000 --- a/pkg/registry/apis/secret/encryption/manager/dek_cache.go +++ /dev/null @@ -1,104 +0,0 @@ -package manager - -import ( - "strconv" - "sync" - "time" - - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" - "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" -) - -type ossDataKeyCache struct { - mtx sync.RWMutex - byId map[string]*encryption.DataKeyCacheEntry - byLabel map[string]*encryption.DataKeyCacheEntry - cacheTTL time.Duration -} - -func ProvideOSSDataKeyCache(cfg *setting.Cfg) encryption.DataKeyCache { - return &ossDataKeyCache{ - byId: make(map[string]*encryption.DataKeyCacheEntry), - byLabel: make(map[string]*encryption.DataKeyCacheEntry), - cacheTTL: cfg.SecretsManagement.DataKeysCacheTTL, - } -} - -func (c *ossDataKeyCache) GetById(id string) (*encryption.DataKeyCacheEntry, bool) { - c.mtx.RLock() - defer c.mtx.RUnlock() - - entry, exists := c.byId[id] - - cacheReadsCounter.With(prometheus.Labels{ - "hit": strconv.FormatBool(exists), - "method": "byId", - }).Inc() - - if !exists || entry.IsExpired() { - return nil, false - } - - return entry, true -} - -func (c *ossDataKeyCache) GetByLabel(label string) (*encryption.DataKeyCacheEntry, bool) { - c.mtx.RLock() - defer c.mtx.RUnlock() - - entry, exists := c.byLabel[label] - - cacheReadsCounter.With(prometheus.Labels{ - "hit": strconv.FormatBool(exists), - "method": "byLabel", - }).Inc() - - if !exists || entry.IsExpired() { - return nil, false - } - - return entry, true -} - -func (c *ossDataKeyCache) AddById(entry *encryption.DataKeyCacheEntry) { - c.mtx.Lock() - defer c.mtx.Unlock() - - entry.Expiration = time.Now().Add(c.cacheTTL) - - c.byId[entry.Id] = entry -} - -func (c *ossDataKeyCache) AddByLabel(entry *encryption.DataKeyCacheEntry) { - c.mtx.Lock() - defer c.mtx.Unlock() - - entry.Expiration = time.Now().Add(c.cacheTTL) - - c.byLabel[entry.Label] = entry -} - -func (c *ossDataKeyCache) RemoveExpired() { - c.mtx.Lock() - defer c.mtx.Unlock() - - for id, entry := range c.byId { - if entry.IsExpired() { - delete(c.byId, id) - } - } - - for label, entry := range c.byLabel { - if entry.IsExpired() { - delete(c.byLabel, label) - } - } -} - -func (c *ossDataKeyCache) Flush() { - c.mtx.Lock() - c.byId = make(map[string]*encryption.DataKeyCacheEntry) - c.byLabel = make(map[string]*encryption.DataKeyCacheEntry) - c.mtx.Unlock() -} diff --git a/pkg/registry/apis/secret/encryption/manager/oss_dek_cache.go b/pkg/registry/apis/secret/encryption/manager/oss_dek_cache.go new file mode 100644 index 00000000000..d8973fab26b --- /dev/null +++ b/pkg/registry/apis/secret/encryption/manager/oss_dek_cache.go @@ -0,0 +1,130 @@ +package manager + +import ( + "strconv" + "sync" + "time" + + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" +) + +type ossDataKeyCache struct { + mtx sync.RWMutex + byId map[string]map[string]*encryption.DataKeyCacheEntry + byLabel map[string]map[string]*encryption.DataKeyCacheEntry + cacheTTL time.Duration +} + +func ProvideOSSDataKeyCache(cfg *setting.Cfg) encryption.DataKeyCache { + return &ossDataKeyCache{ + byId: make(map[string]map[string]*encryption.DataKeyCacheEntry), + byLabel: make(map[string]map[string]*encryption.DataKeyCacheEntry), + cacheTTL: cfg.SecretsManagement.DataKeysCacheTTL, + } +} + +func (c *ossDataKeyCache) GetById(namespace, id string) (_ *encryption.DataKeyCacheEntry, exists bool) { + defer func() { + cacheReadsCounter.With(prometheus.Labels{ + "hit": strconv.FormatBool(exists), + "method": "byId", + }).Inc() + }() + + c.mtx.RLock() + defer c.mtx.RUnlock() + + entries, exists := c.byId[namespace] + if !exists { + return nil, false + } + entry, exists := entries[id] + if !exists || entry.IsExpired() || entry.Namespace != namespace { + return nil, false + } + + return entry, true +} + +func (c *ossDataKeyCache) GetByLabel(namespace, label string) (_ *encryption.DataKeyCacheEntry, exists bool) { + defer func() { + cacheReadsCounter.With(prometheus.Labels{ + "hit": strconv.FormatBool(exists), + "method": "byLabel", + }).Inc() + }() + + c.mtx.RLock() + defer c.mtx.RUnlock() + + entries, exists := c.byLabel[namespace] + if !exists { + return nil, false + } + entry, exists := entries[label] + if !exists || entry.IsExpired() || entry.Namespace != namespace { + return nil, false + } + + return entry, true +} + +func (c *ossDataKeyCache) AddById(namespace string, entry *encryption.DataKeyCacheEntry) { + c.mtx.Lock() + defer c.mtx.Unlock() + + entry.Expiration = time.Now().Add(c.cacheTTL) + entry.Namespace = namespace + + entries, exists := c.byId[namespace] + if !exists { + entries = make(map[string]*encryption.DataKeyCacheEntry) + c.byId[namespace] = entries + } + entries[entry.Id] = entry +} + +func (c *ossDataKeyCache) AddByLabel(namespace string, entry *encryption.DataKeyCacheEntry) { + c.mtx.Lock() + defer c.mtx.Unlock() + + entry.Expiration = time.Now().Add(c.cacheTTL) + entry.Namespace = namespace + + entries, exists := c.byLabel[namespace] + if !exists { + entries = make(map[string]*encryption.DataKeyCacheEntry) + c.byLabel[namespace] = entries + } + entries[entry.Label] = entry +} + +func (c *ossDataKeyCache) RemoveExpired() { + c.mtx.Lock() + defer c.mtx.Unlock() + + for _, entries := range c.byId { + for id, entry := range entries { + if entry.IsExpired() { + delete(entries, id) + } + } + } + + for _, entries := range c.byLabel { + for label, entry := range entries { + if entry.IsExpired() { + delete(entries, label) + } + } + } +} + +func (c *ossDataKeyCache) Flush(namespace string) { + c.mtx.Lock() + c.byId[namespace] = make(map[string]*encryption.DataKeyCacheEntry) + c.byLabel[namespace] = make(map[string]*encryption.DataKeyCacheEntry) + c.mtx.Unlock() +} diff --git a/pkg/registry/apis/secret/encryption/manager/oss_dek_cache_test.go b/pkg/registry/apis/secret/encryption/manager/oss_dek_cache_test.go new file mode 100644 index 00000000000..68c301ca375 --- /dev/null +++ b/pkg/registry/apis/secret/encryption/manager/oss_dek_cache_test.go @@ -0,0 +1,570 @@ +package manager + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOSSDataKeyCache(t *testing.T) { + t.Parallel() + + settings := setting.NewCfg() + settings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 999 * time.Hour, // avoid expiration for testing + } + cache := ProvideOSSDataKeyCache(settings) + + namespace := "test-namespace" + entry := &encryption.DataKeyCacheEntry{ + Id: "key-123", + Label: "2024-01-01@provider.key1", + DataKey: []byte("test-data-key"), + Active: true, + } + + t.Run("AddById and GetById", func(t *testing.T) { + cache.AddById(namespace, entry) + + retrieved, exists := cache.GetById(namespace, entry.Id) + require.True(t, exists, "entry should exist after adding") + assert.Equal(t, entry.Id, retrieved.Id) + assert.Equal(t, entry.Label, retrieved.Label) + assert.Equal(t, entry.DataKey, retrieved.DataKey) + assert.Equal(t, entry.Active, retrieved.Active) + assert.Equal(t, namespace, retrieved.Namespace) + assert.True(t, retrieved.Expiration.After(time.Now()), "expiration should be in the future") + }) + + t.Run("AddByLabel and GetByLabel", func(t *testing.T) { + cache.AddByLabel(namespace, entry) + + retrieved, exists := cache.GetByLabel(namespace, entry.Label) + require.True(t, exists, "entry should exist after adding") + assert.Equal(t, entry.Id, retrieved.Id) + assert.Equal(t, entry.Label, retrieved.Label) + assert.Equal(t, entry.DataKey, retrieved.DataKey) + assert.Equal(t, entry.Active, retrieved.Active) + assert.Equal(t, namespace, retrieved.Namespace) + assert.True(t, retrieved.Expiration.After(time.Now()), "expiration should be in the future") + }) + + t.Run("GetById and GetByLabel are independent", func(t *testing.T) { + cache2 := ProvideOSSDataKeyCache(settings) + ns := "independent-test" + + entryById := &encryption.DataKeyCacheEntry{ + Id: "id-only-key", + Label: "label1", + DataKey: []byte("data1"), + } + entryByLabel := &encryption.DataKeyCacheEntry{ + Id: "id2", + Label: "label-only-key", + DataKey: []byte("data2"), + } + + cache2.AddById(ns, entryById) + cache2.AddByLabel(ns, entryByLabel) + + // Should find by ID + retrieved, exists := cache2.GetById(ns, entryById.Id) + require.True(t, exists) + assert.Equal(t, entryById.Id, retrieved.Id) + + // Should not find by label that wasn't added via AddByLabel + _, exists = cache2.GetByLabel(ns, entryById.Label) + assert.False(t, exists) + + // Should find by label + retrieved, exists = cache2.GetByLabel(ns, entryByLabel.Label) + require.True(t, exists) + assert.Equal(t, entryByLabel.Label, retrieved.Label) + + // Should not find by ID that wasn't added via AddById + _, exists = cache2.GetById(ns, entryByLabel.Id) + assert.False(t, exists) + }) +} + +func TestOSSDataKeyCache_FalseConditions(t *testing.T) { + t.Parallel() + + settings := setting.NewCfg() + settings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 999 * time.Hour, + } + cache := ProvideOSSDataKeyCache(settings) + + namespace := "test-namespace" + entry := &encryption.DataKeyCacheEntry{ + Id: "key-123", + Label: "2024-01-01@provider.key1", + DataKey: []byte("test-data-key"), + Active: true, + } + + t.Run("GetById returns false for non-existent namespace", func(t *testing.T) { + _, exists := cache.GetById("non-existent-namespace", "any-id") + assert.False(t, exists) + }) + + t.Run("GetById returns false for non-existent id", func(t *testing.T) { + cache.AddById(namespace, entry) + _, exists := cache.GetById(namespace, "non-existent-id") + assert.False(t, exists) + }) + + t.Run("GetByLabel returns false for non-existent namespace", func(t *testing.T) { + _, exists := cache.GetByLabel("non-existent-namespace", "any-label") + assert.False(t, exists) + }) + + t.Run("GetByLabel returns false for non-existent label", func(t *testing.T) { + cache.AddByLabel(namespace, entry) + _, exists := cache.GetByLabel(namespace, "non-existent-label") + assert.False(t, exists) + }) + + t.Run("GetById returns false for expired entry", func(t *testing.T) { + shortTTLSettings := setting.NewCfg() + shortTTLSettings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 1 * time.Millisecond, + } + shortCache := ProvideOSSDataKeyCache(shortTTLSettings) + + namespace := "test-ns" + expiredEntry := &encryption.DataKeyCacheEntry{ + Id: "expired-key", + Label: "expired-label", + DataKey: []byte("expired-data"), + } + shortCache.AddById(namespace, expiredEntry) + + time.Sleep(10 * time.Millisecond) + + _, exists := shortCache.GetById(namespace, expiredEntry.Id) + assert.False(t, exists, "should return false for expired entry") + }) + + t.Run("GetByLabel returns false for expired entry", func(t *testing.T) { + shortTTLSettings := setting.NewCfg() + shortTTLSettings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 1 * time.Millisecond, + } + shortCache := ProvideOSSDataKeyCache(shortTTLSettings) + + namespace := "test-ns" + expiredEntry := &encryption.DataKeyCacheEntry{ + Id: "expired-key", + Label: "expired-label", + DataKey: []byte("expired-data"), + } + shortCache.AddByLabel(namespace, expiredEntry) + + time.Sleep(10 * time.Millisecond) + + _, exists := shortCache.GetByLabel(namespace, expiredEntry.Label) + assert.False(t, exists, "should return false for expired entry") + }) + + t.Run("GetById returns false when entry namespace doesn't match", func(t *testing.T) { + // This tests the entry.Namespace != namespace check in GetById + // This is a defensive check that shouldn't normally happen if AddById works correctly + testCache := ProvideOSSDataKeyCache(settings).(*ossDataKeyCache) + + // Manually insert an entry with mismatched namespace to test the defensive check + mismatchedEntry := &encryption.DataKeyCacheEntry{ + Id: "test-id", + Label: "test-label", + DataKey: []byte("test-data"), + Namespace: "wrong-namespace", + Expiration: time.Now().Add(999 * time.Hour), + } + + testCache.mtx.Lock() + testCache.byId["correct-namespace"] = map[string]*encryption.DataKeyCacheEntry{ + mismatchedEntry.Id: mismatchedEntry, + } + testCache.mtx.Unlock() + + _, exists := testCache.GetById("correct-namespace", mismatchedEntry.Id) + assert.False(t, exists, "should return false when entry namespace doesn't match lookup namespace") + }) + + t.Run("GetByLabel returns false when entry namespace doesn't match", func(t *testing.T) { + // This tests the entry.Namespace != namespace check in GetByLabel + testCache := ProvideOSSDataKeyCache(settings).(*ossDataKeyCache) + + // Manually insert an entry with mismatched namespace to test the defensive check + mismatchedEntry := &encryption.DataKeyCacheEntry{ + Id: "test-id", + Label: "test-label", + DataKey: []byte("test-data"), + Namespace: "wrong-namespace", + Expiration: time.Now().Add(999 * time.Hour), + } + + testCache.mtx.Lock() + testCache.byLabel["correct-namespace"] = map[string]*encryption.DataKeyCacheEntry{ + "test-label": mismatchedEntry, + } + testCache.mtx.Unlock() + + _, exists := testCache.GetByLabel("correct-namespace", mismatchedEntry.Label) + assert.False(t, exists, "should return false when entry namespace doesn't match lookup namespace") + }) +} + +// Test namespace isolation +func TestOSSDataKeyCache_NamespaceIsolation(t *testing.T) { + t.Parallel() + + settings := setting.NewCfg() + settings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 999 * time.Hour, + } + cache := ProvideOSSDataKeyCache(settings) + + namespace1 := "namespace-1" + namespace2 := "namespace-2" + + entry1 := &encryption.DataKeyCacheEntry{ + Id: "shared-id", + Label: "shared-label", + DataKey: []byte("data-from-ns1"), + Active: true, + } + + entry2 := &encryption.DataKeyCacheEntry{ + Id: "shared-id", + Label: "shared-label", + DataKey: []byte("data-from-ns2"), + Active: false, + } + + t.Run("entries with same ID in different namespaces are isolated", func(t *testing.T) { + cache.AddById(namespace1, entry1) + cache.AddById(namespace2, entry2) + + retrieved1, exists := cache.GetById(namespace1, entry1.Id) + require.True(t, exists) + assert.Equal(t, entry1.DataKey, retrieved1.DataKey) + assert.Equal(t, namespace1, retrieved1.Namespace) + assert.True(t, retrieved1.Active) + + retrieved2, exists := cache.GetById(namespace2, entry2.Id) + require.True(t, exists) + assert.Equal(t, entry2.DataKey, retrieved2.DataKey) + assert.Equal(t, namespace2, retrieved2.Namespace) + assert.False(t, retrieved2.Active) + }) + + t.Run("entries with same label in different namespaces are isolated", func(t *testing.T) { + cache.AddByLabel(namespace1, entry1) + cache.AddByLabel(namespace2, entry2) + + retrieved1, exists := cache.GetByLabel(namespace1, entry1.Label) + require.True(t, exists) + assert.Equal(t, entry1.DataKey, retrieved1.DataKey) + assert.Equal(t, namespace1, retrieved1.Namespace) + assert.True(t, retrieved1.Active) + + retrieved2, exists := cache.GetByLabel(namespace2, entry2.Label) + require.True(t, exists) + assert.Equal(t, entry2.DataKey, retrieved2.DataKey) + assert.Equal(t, namespace2, retrieved2.Namespace) + assert.False(t, retrieved2.Active) + }) + + t.Run("cannot retrieve entry from wrong namespace", func(t *testing.T) { + // flush both namespaces since the cache is full of stuff now + cache.Flush(namespace1) + cache.Flush(namespace2) + + cache.AddById(namespace1, entry1) + + _, exists := cache.GetById(namespace2, entry1.Id) + assert.False(t, exists, "should not find entry from different namespace") + + cache.AddByLabel(namespace1, entry1) + + _, exists = cache.GetByLabel(namespace2, entry1.Label) + assert.False(t, exists, "should not find entry from different namespace") + }) +} + +func TestOSSDataKeyCache_Expiration(t *testing.T) { + t.Parallel() + t.Run("entries expire after TTL", func(t *testing.T) { + settings := setting.NewCfg() + settings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 50 * time.Millisecond, + } + cache := ProvideOSSDataKeyCache(settings) + + namespace := "test-ns" + entry := &encryption.DataKeyCacheEntry{ + Id: "expiring-key", + Label: "expiring-label", + DataKey: []byte("expiring-data"), + } + + cache.AddById(namespace, entry) + cache.AddByLabel(namespace, entry) + + // Should exist immediately + _, exists := cache.GetById(namespace, entry.Id) + assert.True(t, exists, "entry should exist immediately after adding") + + _, exists = cache.GetByLabel(namespace, entry.Label) + assert.True(t, exists, "entry should exist immediately after adding") + + // Wait for expiration + time.Sleep(100 * time.Millisecond) + + // Should not exist after expiration + _, exists = cache.GetById(namespace, entry.Id) + assert.False(t, exists, "entry should not exist after TTL expires") + + _, exists = cache.GetByLabel(namespace, entry.Label) + assert.False(t, exists, "entry should not exist after TTL expires") + }) + + t.Run("RemoveExpired removes only expired entries", func(t *testing.T) { + settings := setting.NewCfg() + settings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 50 * time.Millisecond, + } + cache := ProvideOSSDataKeyCache(settings) + + namespace := "test-ns" + + // Add entries that will expire + expiredEntry1 := &encryption.DataKeyCacheEntry{ + Id: "expired-1", + Label: "expired-label-1", + DataKey: []byte("expired-data-1"), + } + expiredEntry2 := &encryption.DataKeyCacheEntry{ + Id: "expired-2", + Label: "expired-label-2", + DataKey: []byte("expired-data-2"), + } + + cache.AddById(namespace, expiredEntry1) + cache.AddByLabel(namespace, expiredEntry2) + + // Wait for expiration + time.Sleep(100 * time.Millisecond) + + // Add fresh entries + freshEntry1 := &encryption.DataKeyCacheEntry{ + Id: "fresh-1", + Label: "fresh-label-1", + DataKey: []byte("fresh-data-1"), + } + freshEntry2 := &encryption.DataKeyCacheEntry{ + Id: "fresh-2", + Label: "fresh-label-2", + DataKey: []byte("fresh-data-2"), + } + + cache.AddById(namespace, freshEntry1) + cache.AddByLabel(namespace, freshEntry2) + + // Before RemoveExpired, expired entries still exist in the map + // but GetById/GetByLabel return false due to IsExpired() check + + // Call RemoveExpired + cache.RemoveExpired() + + // Fresh entries should still exist + _, exists := cache.GetById(namespace, freshEntry1.Id) + assert.True(t, exists, "fresh entry should still exist after RemoveExpired") + + _, exists = cache.GetByLabel(namespace, freshEntry2.Label) + assert.True(t, exists, "fresh entry should still exist after RemoveExpired") + + // Expired entries should not exist + ossCache := cache.(*ossDataKeyCache) + _, exists = ossCache.byId[namespace][expiredEntry1.Id] + assert.False(t, exists, "expired entry should not exist after RemoveExpired") + + _, exists = ossCache.byLabel[namespace][expiredEntry2.Label] + assert.False(t, exists, "expired entry should not exist after RemoveExpired") + }) + + t.Run("RemoveExpired handles multiple namespaces", func(t *testing.T) { + settings := setting.NewCfg() + settings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 50 * time.Millisecond, + } + cache := ProvideOSSDataKeyCache(settings) + + ns1 := "namespace-1" + ns2 := "namespace-2" + + ns1ExpiredEntry := &encryption.DataKeyCacheEntry{ + Id: "expired-key-ns1", + Label: "expired-label-ns1", + DataKey: []byte("expired-data"), + } + ns2ExpiredEntry := &encryption.DataKeyCacheEntry{ + Id: "expired-key-ns2", + Label: "expired-label-ns2", + DataKey: []byte("expired-data"), + } + + cache.AddById(ns1, ns1ExpiredEntry) + cache.AddByLabel(ns1, ns1ExpiredEntry) + cache.AddById(ns2, ns2ExpiredEntry) + cache.AddByLabel(ns2, ns2ExpiredEntry) + + time.Sleep(100 * time.Millisecond) + + ns1FreshEntry := &encryption.DataKeyCacheEntry{ + Id: "fresh-key-ns1", + Label: "fresh-label-ns1", + DataKey: []byte("fresh-data-ns1"), + } + ns2FreshEntry := &encryption.DataKeyCacheEntry{ + Id: "fresh-key-ns2", + Label: "fresh-label-ns2", + DataKey: []byte("fresh-data-ns2"), + } + + cache.AddById(ns1, ns1FreshEntry) + cache.AddByLabel(ns1, ns1FreshEntry) + cache.AddById(ns2, ns2FreshEntry) + cache.AddByLabel(ns2, ns2FreshEntry) + + cache.RemoveExpired() + + // Fresh entries in both namespaces should exist + _, exists := cache.GetById(ns1, ns1FreshEntry.Id) + assert.True(t, exists) + + _, exists = cache.GetByLabel(ns1, ns1FreshEntry.Label) + assert.True(t, exists) + + _, exists = cache.GetById(ns2, ns2FreshEntry.Id) + assert.True(t, exists) + + _, exists = cache.GetByLabel(ns2, ns2FreshEntry.Label) + assert.True(t, exists) + + // Expired entries in both namespaces should not exist + ossCache := cache.(*ossDataKeyCache) + _, exists = ossCache.byId[ns1][ns1ExpiredEntry.Id] + assert.False(t, exists) + + _, exists = ossCache.byId[ns2][ns2ExpiredEntry.Id] + assert.False(t, exists) + + _, exists = ossCache.byLabel[ns1][ns1ExpiredEntry.Label] + assert.False(t, exists) + + _, exists = ossCache.byLabel[ns2][ns2ExpiredEntry.Label] + assert.False(t, exists) + }) +} + +// Test Flush() +func TestOSSDataKeyCache_Flush(t *testing.T) { + t.Parallel() + + settings := setting.NewCfg() + settings.SecretsManagement = setting.SecretsManagerSettings{ + DataKeysCacheTTL: 999 * time.Hour, + } + cache := ProvideOSSDataKeyCache(settings) + + namespace1 := "namespace-1" + namespace2 := "namespace-2" + + entry1 := &encryption.DataKeyCacheEntry{ + Id: "key-1", + Label: "label-1", + DataKey: []byte("data-1"), + } + + entry2 := &encryption.DataKeyCacheEntry{ + Id: "key-2", + Label: "label-2", + DataKey: []byte("data-2"), + } + + t.Run("Flush removes all entries from specified namespace", func(t *testing.T) { + cache.AddById(namespace1, entry1) + cache.AddByLabel(namespace1, entry1) + + // Verify entries exist + _, exists := cache.GetById(namespace1, entry1.Id) + require.True(t, exists) + + _, exists = cache.GetByLabel(namespace1, entry1.Label) + require.True(t, exists) + + // Flush namespace1 + cache.Flush(namespace1) + + // Entries should no longer exist + _, exists = cache.GetById(namespace1, entry1.Id) + assert.False(t, exists, "entry should not exist after flush") + + _, exists = cache.GetByLabel(namespace1, entry1.Label) + assert.False(t, exists, "entry should not exist after flush") + }) + + t.Run("Flush only affects specified namespace", func(t *testing.T) { + cache.AddById(namespace1, entry1) + cache.AddByLabel(namespace1, entry1) + cache.AddById(namespace2, entry2) + cache.AddByLabel(namespace2, entry2) + + // Flush only namespace1 + cache.Flush(namespace1) + + // namespace1 entries should not exist + _, exists := cache.GetById(namespace1, entry1.Id) + assert.False(t, exists) + + _, exists = cache.GetByLabel(namespace1, entry1.Label) + assert.False(t, exists) + + // namespace2 entries should still exist + _, exists = cache.GetById(namespace2, entry2.Id) + assert.True(t, exists, "entries in other namespace should not be affected") + + _, exists = cache.GetByLabel(namespace2, entry2.Label) + assert.True(t, exists, "entries in other namespace should not be affected") + }) + + t.Run("Flush on non-existent namespace does not panic", func(t *testing.T) { + assert.NotPanics(t, func() { + cache.Flush("non-existent-namespace") + }) + }) + + t.Run("can add entries after flush", func(t *testing.T) { + cache.AddById(namespace1, entry1) + cache.Flush(namespace1) + + // Add new entry after flush + newEntry := &encryption.DataKeyCacheEntry{ + Id: "new-key", + Label: "new-label", + DataKey: []byte("new-data"), + } + cache.AddById(namespace1, newEntry) + + // New entry should exist + _, exists := cache.GetById(namespace1, "new-key") + assert.True(t, exists, "should be able to add entries after flush") + }) +} diff --git a/pkg/registry/apis/secret/encryption/manager/test_helpers.go b/pkg/registry/apis/secret/encryption/manager/test_helpers.go index 662ca88ca9c..15351a86f50 100644 --- a/pkg/registry/apis/secret/encryption/manager/test_helpers.go +++ b/pkg/registry/apis/secret/encryption/manager/test_helpers.go @@ -59,21 +59,21 @@ func setupTestService(tb testing.TB) *EncryptionManager { type NoopDataKeyCache struct { } -func (c *NoopDataKeyCache) GetById(id string) (*encryption.DataKeyCacheEntry, bool) { +func (c *NoopDataKeyCache) GetById(namespace, id string) (*encryption.DataKeyCacheEntry, bool) { return nil, false } -func (c *NoopDataKeyCache) GetByLabel(label string) (*encryption.DataKeyCacheEntry, bool) { +func (c *NoopDataKeyCache) GetByLabel(namespace, label string) (*encryption.DataKeyCacheEntry, bool) { return nil, false } -func (c *NoopDataKeyCache) AddById(entry *encryption.DataKeyCacheEntry) { +func (c *NoopDataKeyCache) AddById(namespace string, entry *encryption.DataKeyCacheEntry) { } -func (c *NoopDataKeyCache) AddByLabel(entry *encryption.DataKeyCacheEntry) { +func (c *NoopDataKeyCache) AddByLabel(namespace string, entry *encryption.DataKeyCacheEntry) { } func (c *NoopDataKeyCache) RemoveExpired() { } -func (c *NoopDataKeyCache) Flush() {} +func (c *NoopDataKeyCache) Flush(namespace string) {} diff --git a/pkg/registry/apis/secret/encryption/secrets.go b/pkg/registry/apis/secret/encryption/secrets.go index 20fcbe0c56a..0c924ef43c2 100644 --- a/pkg/registry/apis/secret/encryption/secrets.go +++ b/pkg/registry/apis/secret/encryption/secrets.go @@ -42,12 +42,12 @@ func KeyLabel(providerID ProviderID) string { } type DataKeyCache interface { - GetById(id string) (*DataKeyCacheEntry, bool) - GetByLabel(label string) (*DataKeyCacheEntry, bool) - AddById(entry *DataKeyCacheEntry) - AddByLabel(entry *DataKeyCacheEntry) + GetById(namespace, id string) (*DataKeyCacheEntry, bool) + GetByLabel(namespace, label string) (*DataKeyCacheEntry, bool) + AddById(namespace string, entry *DataKeyCacheEntry) + AddByLabel(namespace string, entry *DataKeyCacheEntry) RemoveExpired() - Flush() + Flush(namespace string) } type DataKeyCacheEntry struct {