SecretsManager: Add base encryption manager (#107562)

Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
This commit is contained in:
Dana Axinte
2025-07-03 11:29:14 +01:00
committed by GitHub
co-authored by Michael Mandrus Matheus Macabu
parent 93c14c52da
commit 4d8678c7f2
28 changed files with 1173 additions and 431 deletions
@@ -2,20 +2,29 @@ package sqlkeeper
import (
"context"
"encoding/base64"
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
encryptionmanager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/secret/database"
encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
// Make this a `TestIntegration<name>` once we have the real storage implementation
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func Test_SQLKeeperSetup(t *testing.T) {
ctx := context.Background()
namespace1 := "namespace1"
@@ -24,50 +33,57 @@ func Test_SQLKeeperSetup(t *testing.T) {
plaintext2 := "very secret string in namespace 2"
nonExistentID := contracts.ExternalID("non existent")
cfg := setting.NewCfg()
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "sdDkslslld",
EncryptionProvider: "secretKey.v1",
},
}
sqlKeeper, err := setupTestService(t, cfg)
require.NoError(t, err)
require.NotNil(t, sqlKeeper)
keeperCfg := &secretv0alpha1.SystemKeeperConfig{}
t.Run("storing an encrypted value returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1)
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
externalId2, err := sqlKeeper.Store(ctx, nil, namespace2, plaintext2)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext2)
require.NoError(t, err)
require.NotEmpty(t, externalId2)
t.Run("expose the encrypted value from existing namespace", func(t *testing.T) {
exposedVal1, err := sqlKeeper.Expose(ctx, nil, namespace1, externalId1)
exposedVal1, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId1)
require.NoError(t, err)
require.NotNil(t, exposedVal1)
assert.Equal(t, plaintext1, exposedVal1.DangerouslyExposeAndConsumeValue())
exposedVal2, err := sqlKeeper.Expose(ctx, nil, namespace2, externalId2)
exposedVal2, err := sqlKeeper.Expose(ctx, keeperCfg, namespace2, externalId2)
require.NoError(t, err)
require.NotNil(t, exposedVal2)
assert.Equal(t, plaintext2, exposedVal2.DangerouslyExposeAndConsumeValue())
})
t.Run("expose encrypted value from different namespace returns error", func(t *testing.T) {
exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace2, externalId1)
exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace2, externalId1)
require.Error(t, err)
assert.Empty(t, exposedVal)
exposedVal, err = sqlKeeper.Expose(ctx, nil, namespace1, externalId2)
exposedVal, err = sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId2)
require.Error(t, err)
assert.Empty(t, exposedVal)
})
})
t.Run("storing same value in same namespace returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1)
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
externalId2, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId2)
@@ -75,11 +91,11 @@ func Test_SQLKeeperSetup(t *testing.T) {
})
t.Run("storing same value in different namespace returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1)
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
externalId2, err := sqlKeeper.Store(ctx, nil, namespace2, plaintext1)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId2)
@@ -87,46 +103,46 @@ func Test_SQLKeeperSetup(t *testing.T) {
})
t.Run("exposing non existing values returns error", func(t *testing.T) {
exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, nonExistentID)
exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, nonExistentID)
require.Error(t, err)
assert.Empty(t, exposedVal)
})
t.Run("deleting an existing encrypted value does not return error", func(t *testing.T) {
externalID, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1)
externalID, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalID)
exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, externalID)
exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalID)
require.NoError(t, err)
assert.NotNil(t, exposedVal)
assert.Equal(t, plaintext1, exposedVal.DangerouslyExposeAndConsumeValue())
err = sqlKeeper.Delete(ctx, nil, namespace1, externalID)
err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, externalID)
require.NoError(t, err)
})
t.Run("deleting an non existing encrypted value does not return error", func(t *testing.T) {
err = sqlKeeper.Delete(ctx, nil, namespace1, nonExistentID)
err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, nonExistentID)
require.NoError(t, err)
})
t.Run("updating an existent encrypted value returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1)
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
err = sqlKeeper.Update(ctx, nil, namespace1, externalId1, plaintext2)
err = sqlKeeper.Update(ctx, keeperCfg, namespace1, externalId1, plaintext2)
require.NoError(t, err)
exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, externalId1)
exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId1)
require.NoError(t, err)
assert.NotNil(t, exposedVal)
assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue())
})
t.Run("updating a non existent encrypted value returns error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1)
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
@@ -136,104 +152,33 @@ func Test_SQLKeeperSetup(t *testing.T) {
}
func setupTestService(t *testing.T, cfg *setting.Cfg) (*SQLKeeper, error) {
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
database := database.ProvideDatabase(testDB, tracer)
// Initialize the encryption manager with in-memory implementation
encMgr := &inMemoryEncryptionManager{}
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
// Initialize encrypted value storage with in-memory implementation
encValueStore := newInMemoryEncryptedValueStorage()
// Initialize the encryption manager
dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
encMgr, err := encryptionmanager.ProvideEncryptionManager(
tracer,
dataKeyStore,
cfg,
usageStats,
nil,
)
require.NoError(t, err)
// Initialize encrypted value storage with a fake db
encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features)
require.NoError(t, err)
// Initialize the SQLKeeper
sqlKeeper := NewSQLKeeper(tracer, encMgr, encValueStore)
return sqlKeeper, nil
}
// While we don't have the real implementation, use an in-memory one
type inMemoryEncryptionManager struct{}
func (m *inMemoryEncryptionManager) Encrypt(_ context.Context, _ string, value []byte) ([]byte, error) {
return []byte(base64.StdEncoding.EncodeToString(value)), nil
}
func (m *inMemoryEncryptionManager) Decrypt(_ context.Context, _ string, value []byte) ([]byte, error) {
return base64.StdEncoding.DecodeString(string(value))
}
func (m *inMemoryEncryptionManager) ReEncryptDataKeys(_ context.Context, _ string) error {
return nil
}
func (m *inMemoryEncryptionManager) RotateDataKeys(_ context.Context, _ string) error {
return nil
}
// While we don't have the real implementation, use an in-memory one
type inMemoryEncryptedValueStorage struct {
mu sync.RWMutex
store map[string]*contracts.EncryptedValue
}
func newInMemoryEncryptedValueStorage() *inMemoryEncryptedValueStorage {
return &inMemoryEncryptedValueStorage{
store: make(map[string]*contracts.EncryptedValue),
}
}
func (m *inMemoryEncryptedValueStorage) Create(_ context.Context, namespace string, encryptedData []byte) (*contracts.EncryptedValue, error) {
m.mu.Lock()
defer m.mu.Unlock()
uid := fmt.Sprintf("%d", len(m.store)+1) // Generate simple incremental IDs
encValue := &contracts.EncryptedValue{
UID: uid,
Namespace: namespace,
EncryptedData: encryptedData,
Created: 1, // Dummy timestamp
Updated: 1, // Dummy timestamp
}
compositeKey := namespace + ":" + uid
m.store[compositeKey] = encValue
return encValue, nil
}
func (m *inMemoryEncryptedValueStorage) Get(_ context.Context, namespace string, uid string) (*contracts.EncryptedValue, error) {
m.mu.RLock()
defer m.mu.RUnlock()
compositeKey := namespace + ":" + uid
encValue, exists := m.store[compositeKey]
if !exists {
return nil, fmt.Errorf("value not found for namespace %s and uid %s", namespace, uid)
}
return encValue, nil
}
func (m *inMemoryEncryptedValueStorage) Delete(_ context.Context, namespace string, uid string) error {
m.mu.Lock()
defer m.mu.Unlock()
compositeKey := namespace + ":" + uid
delete(m.store, compositeKey)
return nil
}
func (m *inMemoryEncryptedValueStorage) Update(_ context.Context, namespace string, uid string, encryptedData []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
compositeKey := namespace + ":" + uid
encValue, exists := m.store[compositeKey]
if !exists {
return fmt.Errorf("value not found for namespace %s and uid %s", namespace, uid)
}
encValue.EncryptedData = encryptedData
encValue.Updated = 2 // Update timestamp
return nil
}