Secrets: encryption encryption storage uses versioning (#108036)

* Secrets: delete unused FakeKeeper

* Secrets: encrypted value storage stores versions

* add version to span

* trigger build

* remove ineffectual assignment

* lint

* drop secret_encrypted_value.uid / add name and version columns
This commit is contained in:
Bruno
2025-07-14 09:28:07 -03:00
committed by GitHub
parent afe6cd8a6d
commit baa89f3eac
31 changed files with 454 additions and 302 deletions
@@ -36,8 +36,13 @@ func NewSQLKeeper(
}
}
func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, exposedValueOrRef string) (contracts.ExternalID, error) {
ctx, span := s.tracer.Start(ctx, "SQLKeeper.Store", trace.WithAttributes(attribute.String("namespace", namespace)))
func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) {
ctx, span := s.tracer.Start(ctx, "SQLKeeper.Store",
trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("name", name),
attribute.Int64("version", version)),
)
defer span.End()
start := time.Now()
@@ -46,27 +51,28 @@ func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, n
return "", fmt.Errorf("unable to encrypt value: %w", err)
}
encryptedVal, err := s.store.Create(ctx, namespace, encryptedData)
_, err = s.store.Create(ctx, namespace, name, version, encryptedData)
if err != nil {
return "", fmt.Errorf("unable to store encrypted value: %w", err)
}
s.metrics.StoreDuration.WithLabelValues(string(cfg.Type())).Observe(time.Since(start).Seconds())
externalID := contracts.ExternalID(encryptedVal.UID)
span.SetAttributes(attribute.String("externalID", externalID.String()))
return externalID, nil
// An external id is not required to interact with the sql keeper.
// An empty string is returned just to comply with the Keeper interface.
return contracts.ExternalID(""), nil
}
func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID contracts.ExternalID) (secretv1beta1.ExposedSecureValue, error) {
func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) {
ctx, span := s.tracer.Start(ctx, "SQLKeeper.Expose", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("externalID", externalID.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
defer span.End()
start := time.Now()
encryptedValue, err := s.store.Get(ctx, namespace, externalID.String())
encryptedValue, err := s.store.Get(ctx, namespace, name, version)
if err != nil {
return "", fmt.Errorf("unable to get encrypted value: %w", err)
}
@@ -82,15 +88,16 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig,
return exposedValue, nil
}
func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID contracts.ExternalID) error {
func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error {
ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("externalID", externalID.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
defer span.End()
start := time.Now()
err := s.store.Delete(ctx, namespace, externalID.String())
err := s.store.Delete(ctx, namespace, name, version)
if err != nil {
return fmt.Errorf("failed to delete encrypted value: %w", err)
}
@@ -100,10 +107,11 @@ func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig,
return nil
}
func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID contracts.ExternalID, exposedValueOrRef string) error {
func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error {
ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("externalID", externalID.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
defer span.End()
@@ -113,7 +121,7 @@ func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig,
return fmt.Errorf("unable to encrypt value: %w", err)
}
err = s.store.Update(ctx, namespace, externalID.String(), encryptedData)
err = s.store.Update(ctx, namespace, name, version, encryptedData)
if err != nil {
return fmt.Errorf("failed to update encrypted value: %w", err)
}
@@ -1,23 +1,13 @@
package sqlkeeper
package sqlkeeper_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"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/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
@@ -26,159 +16,133 @@ func TestMain(m *testing.M) {
}
func Test_SQLKeeperSetup(t *testing.T) {
ctx := context.Background()
namespace1 := "namespace1"
name1 := "name1"
version1 := int64(1)
namespace2 := "namespace2"
name2 := "name2"
plaintext1 := "very secret string in namespace 1"
plaintext2 := "very secret string in namespace 2"
nonExistentID := contracts.ExternalID("non existent")
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 := &secretv1beta1.SystemKeeperConfig{}
t.Run("storing an encrypted value returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
sut := testutils.Setup(t)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext2)
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
_, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace2, name2, version1, 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, keeperCfg, namespace1, externalId1)
sut := testutils.Setup(t)
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
exposedVal1, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1)
require.NoError(t, err)
require.NotNil(t, exposedVal1)
assert.Equal(t, plaintext1, exposedVal1.DangerouslyExposeAndConsumeValue())
exposedVal2, err := sqlKeeper.Expose(ctx, keeperCfg, namespace2, externalId2)
_, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace2, name2, version1, plaintext2)
require.NoError(t, err)
exposedVal2, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace2, name2, version1)
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, keeperCfg, namespace2, externalId1)
sut := testutils.Setup(t)
_, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace2, name1, version1)
require.Error(t, err)
assert.Empty(t, exposedVal)
exposedVal, err = sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId2)
exposedVal, err = sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name2, version1)
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, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
t.Run("storing same value in same namespace returns error", func(t *testing.T) {
sut := testutils.Setup(t)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId2)
assert.NotEqual(t, externalId1, externalId2)
_, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NotNil(t, err)
})
t.Run("storing same value in different namespace returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
sut := testutils.Setup(t)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext1)
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId2)
assert.NotEqual(t, externalId1, externalId2)
_, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace2, name1, version1, plaintext1)
require.NoError(t, err)
})
t.Run("exposing non existing values returns error", func(t *testing.T) {
exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, nonExistentID)
sut := testutils.Setup(t)
exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, "non_existing_name", version1)
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, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalID)
sut := testutils.Setup(t)
exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalID)
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1)
require.NoError(t, err)
assert.NotNil(t, exposedVal)
assert.Equal(t, plaintext1, exposedVal.DangerouslyExposeAndConsumeValue())
err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, externalID)
err = sut.SQLKeeper.Delete(t.Context(), keeperCfg, namespace1, name1, version1)
require.NoError(t, err)
})
t.Run("deleting an non existing encrypted value does not return error", func(t *testing.T) {
err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, nonExistentID)
sut := testutils.Setup(t)
err := sut.SQLKeeper.Delete(t.Context(), keeperCfg, namespace1, "non_existing_name", version1)
require.NoError(t, err)
})
t.Run("updating an existent encrypted value returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
sut := testutils.Setup(t)
err = sqlKeeper.Update(ctx, keeperCfg, namespace1, externalId1, plaintext2)
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId1)
err = sut.SQLKeeper.Update(t.Context(), keeperCfg, namespace1, name1, version1, plaintext2)
require.NoError(t, err)
exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1)
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, keeperCfg, namespace1, plaintext1)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
sut := testutils.Setup(t)
err = sqlKeeper.Update(ctx, nil, namespace1, nonExistentID, plaintext2)
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err)
err = sut.SQLKeeper.Update(t.Context(), nil, namespace1, "non_existing_name", version1, plaintext2)
require.Error(t, err)
})
}
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)
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
// Initialize the encryption manager
dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil)
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, nil)
return sqlKeeper, nil
}