The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/registry/apis/secret/encryption/manager/manager_test.go

622 lines
22 KiB

package manager
import (
"context"
"errors"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
"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"
"github.com/grafana/grafana/pkg/util"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestEncryptionService_EnvelopeEncryption(t *testing.T) {
svc := setupTestService(t)
ctx := context.Background()
namespace := "test-namespace"
t.Run("encrypting should create DEK", func(t *testing.T) {
plaintext := []byte("very secret string")
encrypted, err := svc.Encrypt(context.Background(), namespace, plaintext)
require.NoError(t, err)
decrypted, err := svc.Decrypt(context.Background(), namespace, encrypted)
require.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
keys, err := svc.store.GetAllDataKeys(ctx, namespace)
require.NoError(t, err)
assert.Equal(t, len(keys), 1)
})
t.Run("encrypting another secret should use the same DEK", func(t *testing.T) {
plaintext := []byte("another very secret string")
encrypted, err := svc.Encrypt(context.Background(), namespace, plaintext)
require.NoError(t, err)
decrypted, err := svc.Decrypt(context.Background(), namespace, encrypted)
require.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
keys, err := svc.store.GetAllDataKeys(ctx, namespace)
require.NoError(t, err)
assert.Equal(t, len(keys), 1)
})
t.Run("usage stats should be registered", func(t *testing.T) {
reports, err := svc.usageStats.GetUsageReport(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.current_provider.secret_key.count"])
assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.providers.secret_key.count"])
})
}
func TestEncryptionService_DataKeys(t *testing.T) {
// Initialize data key storage with a fake db
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
features := featuremgmt.WithFeatures(featuremgmt.FlagSecretsManagementAppPlatform)
tracer := noop.NewTracerProvider().Tracer("test")
store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
ctx := context.Background()
namespace := "test-namespace"
dataKey := &contracts.SecretDataKey{
UID: util.GenerateShortUID(),
Label: "test1",
Active: true,
Provider: "test",
EncryptedData: []byte{0x62, 0xAF, 0xA1, 0x1A},
Namespace: namespace,
}
t.Run("querying for a DEK that does not exist", func(t *testing.T) {
res, err := store.GetDataKey(ctx, namespace, dataKey.UID)
assert.ErrorIs(t, contracts.ErrDataKeyNotFound, err)
assert.Nil(t, res)
})
t.Run("creating an active DEK", func(t *testing.T) {
err := store.CreateDataKey(ctx, dataKey)
require.NoError(t, err)
res, err := store.GetDataKey(ctx, namespace, dataKey.UID)
require.NoError(t, err)
assert.Equal(t, dataKey.EncryptedData, res.EncryptedData)
assert.Equal(t, dataKey.Provider, res.Provider)
assert.Equal(t, dataKey.Label, res.Label)
assert.Equal(t, dataKey.UID, res.UID)
assert.True(t, dataKey.Active)
current, err := store.GetCurrentDataKey(ctx, namespace, dataKey.Label)
require.NoError(t, err)
assert.Equal(t, dataKey.EncryptedData, current.EncryptedData)
assert.Equal(t, dataKey.Provider, current.Provider)
assert.Equal(t, dataKey.Label, current.Label)
assert.Equal(t, dataKey.UID, current.UID)
assert.True(t, current.Active)
})
t.Run("creating an inactive DEK", func(t *testing.T) {
k := &contracts.SecretDataKey{
UID: util.GenerateShortUID(),
Namespace: namespace,
Active: false,
Label: "test2",
Provider: "test",
EncryptedData: []byte{0x62, 0xAF, 0xA1, 0x1A},
}
err := store.CreateDataKey(ctx, k)
require.Error(t, err)
res, err := store.GetDataKey(ctx, namespace, k.UID)
assert.Equal(t, contracts.ErrDataKeyNotFound, err)
assert.Nil(t, res)
})
t.Run("deleting DEK when no id provided must fail", func(t *testing.T) {
beforeDelete, err := store.GetAllDataKeys(ctx, namespace)
require.NoError(t, err)
err = store.DeleteDataKey(ctx, namespace, "")
require.Error(t, err)
afterDelete, err := store.GetAllDataKeys(ctx, namespace)
require.NoError(t, err)
assert.Equal(t, beforeDelete, afterDelete)
})
t.Run("deleting a DEK", func(t *testing.T) {
err := store.DeleteDataKey(ctx, namespace, dataKey.UID)
require.NoError(t, err)
res, err := store.GetDataKey(ctx, namespace, dataKey.UID)
assert.Equal(t, contracts.ErrDataKeyNotFound, err)
assert.Nil(t, res)
})
}
func TestEncryptionService_UseCurrentProvider(t *testing.T) {
t.Run("When encryption_provider is not specified explicitly, should use 'secretKey' as a current provider", func(t *testing.T) {
svc := setupTestService(t)
assert.Equal(t, encryption.ProviderID("secret_key.v1"), svc.providerConfig.CurrentProvider)
})
t.Run("Should use encrypt/decrypt methods of the current encryption provider", func(t *testing.T) {
rawCfg := `
[secrets_manager.encryption.fakeProvider.v1]
`
raw, err := ini.Load([]byte(rawCfg))
require.NoError(t, err)
cfg := &setting.Cfg{
Raw: raw,
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}},
},
}
features := featuremgmt.WithFeatures(featuremgmt.FlagSecretsManagementAppPlatform)
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
require.NoError(t, err)
encMgr, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
encryptionManager := encMgr.(*EncryptionManager)
//override default provider with fake, and register the fake separately
fake := &fakeProvider{}
encryptionManager.providerConfig.AvailableProviders = encryption.ProviderMap{
encryption.ProviderID("fakeProvider.v1"): fake,
}
encryptionManager.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1")
namespace := "test-namespace"
encrypted, _ := encryptionManager.Encrypt(context.Background(), namespace, []byte{})
assert.True(t, fake.encryptCalled)
assert.False(t, fake.decryptCalled)
// encryption manager tries to find a DEK in a cache first before calling provider's decrypt
// to bypass the cache, we set up one more secrets service to test decrypting
svcDecryptMgr, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
svcDecrypt := svcDecryptMgr.(*EncryptionManager)
svcDecrypt.providerConfig.AvailableProviders = encryption.ProviderMap{
encryption.ProviderID("fakeProvider.v1"): fake,
}
svcDecrypt.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1")
_, _ = svcDecrypt.Decrypt(context.Background(), namespace, encrypted)
assert.True(t, fake.decryptCalled, "fake provider's decrypt should be called")
})
}
func TestEncryptionService_SecretKeyVersionUpgrade(t *testing.T) {
ctx := context.Background()
namespace := "test-namespace"
// Generate random keys for testing
oldKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars
newKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars
t.Run("should encrypt with v1, upgrade to v2, encrypt with v2, and decrypt both", func(t *testing.T) {
// Step 1: Set up v1 configuration
cfgV1 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": oldKey}},
},
}
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
tracer := noop.NewTracerProvider().Tracer("test")
encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfgV1, enc)
require.NoError(t, err)
svcV1, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
// Step 2: Encrypt something with v1
plaintext := []byte("secret data from v1")
encryptedV1, err := svcV1.Encrypt(ctx, namespace, plaintext)
require.NoError(t, err)
// Verify v1 can decrypt its own data
decryptedV1, err := svcV1.Decrypt(ctx, namespace, encryptedV1)
require.NoError(t, err)
assert.Equal(t, plaintext, decryptedV1)
// Verify current provider is v1
encMgrV1 := svcV1.(*EncryptionManager)
assert.Equal(t, encryption.ProviderID("secret_key.v1"), encMgrV1.providerConfig.CurrentProvider)
// Step 3: Create new configuration with v2 as current provider
cfgV2 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v2",
ConfiguredKMSProviders: map[string]map[string]string{
"secret_key.v1": {"secret_key": oldKey},
"secret_key.v2": {"secret_key": newKey},
},
},
}
// Reinitialize service with v2 configuration (reuse same store)
ossProvidersV2, err := osskmsproviders.ProvideOSSKMSProviders(cfgV2, enc)
require.NoError(t, err)
svcV2, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProvidersV2,
)
require.NoError(t, err)
// Step 4: Ensure we can encrypt and decrypt with the new key (v2)
newPlaintext := []byte("secret data from v2")
encryptedV2, err := svcV2.Encrypt(ctx, namespace, newPlaintext)
require.NoError(t, err)
decryptedV2, err := svcV2.Decrypt(ctx, namespace, encryptedV2)
require.NoError(t, err)
assert.Equal(t, newPlaintext, decryptedV2)
// Verify current provider is v2
encMgrV2 := svcV2.(*EncryptionManager)
assert.Equal(t, encryption.ProviderID("secret_key.v2"), encMgrV2.providerConfig.CurrentProvider)
// Step 5: Ensure we can decrypt the old value encrypted with v1
decryptedOldWithV2, err := svcV2.Decrypt(ctx, namespace, encryptedV1)
require.NoError(t, err)
assert.Equal(t, plaintext, decryptedOldWithV2)
// Verify both providers are available
assert.Contains(t, encMgrV2.providerConfig.AvailableProviders, encryption.ProviderID("secret_key.v1"))
assert.Contains(t, encMgrV2.providerConfig.AvailableProviders, encryption.ProviderID("secret_key.v2"))
assert.Equal(t, 2, len(encMgrV2.providerConfig.AvailableProviders))
})
t.Run("encrypting with v1 then removing the v1 config should cause decryption to fail", func(t *testing.T) {
tracer := noop.NewTracerProvider().Tracer("test")
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
cfgV1 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{
"secret_key.v1": {"secret_key": uuid.New().String()},
},
},
}
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfgV1, enc)
require.NoError(t, err)
svcV1, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
rsp, err := svcV1.Encrypt(ctx, namespace, []byte("test"))
require.NoError(t, err)
cfgV2 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v2",
ConfiguredKMSProviders: map[string]map[string]string{
"secret_key.v2": {"secret_key": uuid.New().String()},
},
},
}
ossProvidersV2, err := osskmsproviders.ProvideOSSKMSProviders(cfgV2, enc)
require.NoError(t, err)
svcV2, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProvidersV2,
)
require.NoError(t, err)
_, err = svcV2.Decrypt(ctx, namespace, rsp)
require.Error(t, err)
})
}
type fakeProvider struct {
encryptCalled bool
decryptCalled bool
}
func (p *fakeProvider) Encrypt(_ context.Context, _ []byte) ([]byte, error) {
p.encryptCalled = true
return []byte{}, nil
}
func (p *fakeProvider) Decrypt(_ context.Context, _ []byte) ([]byte, error) {
p.decryptCalled = true
return []byte{}, nil
}
func TestEncryptionService_Decrypt(t *testing.T) {
ctx := context.Background()
namespace := "test-namespace"
t.Run("empty payload should fail", func(t *testing.T) {
svc := setupTestService(t)
_, err := svc.Decrypt(context.Background(), namespace, []byte(""))
require.Error(t, err)
assert.Equal(t, "unable to decrypt empty payload", err.Error())
})
t.Run("ee encrypted payload with ee enabled should work", func(t *testing.T) {
svc := setupTestService(t)
ciphertext, err := svc.Encrypt(ctx, namespace, []byte("grafana"))
require.NoError(t, err)
plaintext, err := svc.Decrypt(ctx, namespace, ciphertext)
assert.NoError(t, err)
assert.Equal(t, []byte("grafana"), plaintext)
})
}
func TestIntegration_SecretsService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
someData := []byte(`some-data`)
namespace := "test-namespace"
tcs := map[string]func(*testing.T, db.DB, contracts.EncryptionManager){
"regular": func(t *testing.T, _ db.DB, svc contracts.EncryptionManager) {
// We encrypt some data normally, no transactions implied.
_, err := svc.Encrypt(ctx, namespace, someData)
require.NoError(t, err)
},
"within successful InTransaction": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) {
require.NoError(t, store.InTransaction(ctx, func(ctx context.Context) error {
// We encrypt some data within a transaction that shares the db session.
_, err := svc.Encrypt(ctx, namespace, someData)
require.NoError(t, err)
// And the transition succeeds.
return nil
}))
},
"within unsuccessful InTransaction": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) {
require.NotNil(t, store.InTransaction(ctx, func(ctx context.Context) error {
// We encrypt some data within a transaction that shares the db session.
_, err := svc.Encrypt(ctx, namespace, someData)
require.NoError(t, err)
// But the transaction fails.
return errors.New("error")
}))
},
"within unsuccessful InTransaction (plus forced db fetch)": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) {
require.NotNil(t, store.InTransaction(ctx, func(ctx context.Context) error {
// We encrypt some data within a transaction that shares the db session.
encrypted, err := svc.Encrypt(ctx, namespace, someData)
require.NoError(t, err)
// At this point the data key is not cached yet because
// the transaction haven't been committed yet,
// and won't, so we do a decrypt operation within the
// transaction to force the data key to be
// (potentially) cached (it shouldn't to prevent issues).
decrypted, err := svc.Decrypt(ctx, namespace, encrypted)
require.NoError(t, err)
assert.Equal(t, someData, decrypted)
// But the transaction fails.
return errors.New("error")
}))
},
"within successful WithTransactionalDbSession": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) {
require.NoError(t, store.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// We encrypt some data within a transaction that does not share the db session.
_, err := svc.Encrypt(ctx, namespace, someData)
require.NoError(t, err)
// And the transition succeeds.
return nil
}))
},
"within unsuccessful WithTransactionalDbSession": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) {
require.NotNil(t, store.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// We encrypt some data within a transaction that does not share the db session.
_, err := svc.Encrypt(ctx, namespace, someData)
require.NoError(t, err)
// But the transaction fails.
return errors.New("error")
}))
},
"within unsuccessful WithTransactionalDbSession (plus forced db fetch)": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) {
require.NotNil(t, store.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// We encrypt some data within a transaction that does not share the db session.
encrypted, err := svc.Encrypt(ctx, namespace, someData)
require.NoError(t, err)
// At this point the data key is not cached yet because
// the transaction haven't been committed yet,
// and won't, so we do a decrypt operation within the
// transaction to force the data key to be
// (potentially) cached (it shouldn't to prevent issues).
decrypted, err := svc.Decrypt(ctx, namespace, encrypted)
require.NoError(t, err)
assert.Equal(t, someData, decrypted)
// But the transaction fails.
return errors.New("error")
}))
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
features := featuremgmt.WithFeatures(featuremgmt.FlagSecretsManagementAppPlatform)
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}},
},
}
store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
require.NoError(t, err)
svc, err := ProvideEncryptionManager(
tracer,
store,
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
ctx := context.Background()
namespace := "test-namespace"
// Here's what actually matters and varies on each test: look at the test case name.
//
// For historical reasons, and in an old implementation, when a successful encryption
// operation happened within an unsuccessful transaction, the data key was used to be
// cached in memory for the next encryption operations, which caused some data to be
// encrypted with a data key that haven't actually been persisted into the database.
tc(t, testDB, svc)
// Therefore, the data encrypted after this point, become unrecoverable after a restart.
// So, the different test cases here are there to prevent that from happening again
// in the future, whatever it is what happens.
// So, we proceed with an encryption operation:
toEncrypt := []byte(`data-to-encrypt`)
encrypted, err := svc.Encrypt(ctx, namespace, toEncrypt)
require.NoError(t, err)
// And then, we MUST still be able to decrypt the previously encrypted data:
decrypted, err := svc.Decrypt(ctx, namespace, encrypted)
require.NoError(t, err)
assert.Equal(t, toEncrypt, decrypted)
})
}
}
func TestEncryptionService_ThirdPartyProviders(t *testing.T) {
tracer := noop.NewTracerProvider().Tracer("test")
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
svc, err := ProvideEncryptionManager(
tracer,
nil,
usageStats,
enc,
encryption.ProviderConfig{
CurrentProvider: encryption.ProviderID("fakeProvider.v1"),
AvailableProviders: encryption.ProviderMap{
encryption.ProviderID("fakeProvider.v1"): &fakeProvider{},
},
},
)
require.NoError(t, err)
encMgr := svc.(*EncryptionManager)
require.Len(t, encMgr.providerConfig.AvailableProviders, 1)
require.Contains(t, encMgr.providerConfig.AvailableProviders, encryption.ProviderID("fakeProvider.v1"))
}