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
@@ -13,16 +13,17 @@ type EncryptionManager interface {
} }
type EncryptedValue struct { type EncryptedValue struct {
UID string
Namespace string Namespace string
Name string
Version int64
EncryptedData []byte EncryptedData []byte
Created int64 Created int64
Updated int64 Updated int64
} }
type EncryptedValueStorage interface { type EncryptedValueStorage interface {
Create(ctx context.Context, namespace string, encryptedData []byte) (*EncryptedValue, error) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (*EncryptedValue, error)
Update(ctx context.Context, namespace string, uid string, encryptedData []byte) error Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error
Get(ctx context.Context, namespace string, uid string) (*EncryptedValue, error) Get(ctx context.Context, namespace, name string, version int64) (*EncryptedValue, error)
Delete(ctx context.Context, namespace string, uid string) error Delete(ctx context.Context, namespace, name string, version int64) error
} }
+4 -4
View File
@@ -95,10 +95,10 @@ func (s ExternalID) String() string {
// Keeper is the interface for secret keepers. // Keeper is the interface for secret keepers.
type Keeper interface { type Keeper interface {
Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, exposedValueOrRef string) (ExternalID, error) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error)
Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID ExternalID, exposedValueOrRef string) error Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error
Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID ExternalID) (secretv1beta1.ExposedSecureValue, error) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error)
Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID ExternalID) error Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error
} }
// Service is the interface for secret keeper services. // Service is the interface for secret keeper services.
@@ -1,68 +0,0 @@
package fakes
import (
"context"
"errors"
"github.com/google/uuid"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
)
var ErrSecretNotFound = errors.New("secret not found")
type FakeKeeper struct {
values map[string]map[string]string
}
var _ contracts.Keeper = (*FakeKeeper)(nil)
func NewFakeKeeper() *FakeKeeper {
return &FakeKeeper{
values: make(map[string]map[string]string),
}
}
func (s *FakeKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, exposedValueOrRef string) (contracts.ExternalID, error) {
ns, ok := s.values[namespace]
if !ok {
ns = make(map[string]string)
}
uid := uuid.New().String()
ns[uid] = exposedValueOrRef
s.values[namespace] = ns
return contracts.ExternalID(uid), nil
}
func (s *FakeKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID contracts.ExternalID) (secretv1beta1.ExposedSecureValue, error) {
ns, ok := s.values[namespace]
if !ok {
return "", ErrSecretNotFound
}
exposedVal, ok := ns[externalID.String()]
if !ok {
return "", ErrSecretNotFound
}
return secretv1beta1.NewExposedSecureValue(exposedVal), nil
}
func (s *FakeKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID contracts.ExternalID) error {
return nil
}
func (s *FakeKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, externalID contracts.ExternalID, exposedValueOrRef string) error {
ns, ok := s.values[namespace]
if !ok {
return ErrSecretNotFound
}
_, ok = ns[externalID.String()]
if !ok {
return ErrSecretNotFound
}
ns[externalID.String()] = exposedValueOrRef
return nil
}
@@ -36,8 +36,13 @@ func NewSQLKeeper(
} }
} }
func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace string, exposedValueOrRef string) (contracts.ExternalID, error) { 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))) 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() defer span.End()
start := time.Now() 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) 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 { if err != nil {
return "", fmt.Errorf("unable to store encrypted value: %w", err) return "", fmt.Errorf("unable to store encrypted value: %w", err)
} }
s.metrics.StoreDuration.WithLabelValues(string(cfg.Type())).Observe(time.Since(start).Seconds()) 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( ctx, span := s.tracer.Start(ctx, "SQLKeeper.Expose", trace.WithAttributes(
attribute.String("namespace", namespace), attribute.String("namespace", namespace),
attribute.String("externalID", externalID.String()), attribute.String("name", name),
attribute.Int64("version", version),
)) ))
defer span.End() defer span.End()
start := time.Now() start := time.Now()
encryptedValue, err := s.store.Get(ctx, namespace, externalID.String()) encryptedValue, err := s.store.Get(ctx, namespace, name, version)
if err != nil { if err != nil {
return "", fmt.Errorf("unable to get encrypted value: %w", err) 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 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( ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes(
attribute.String("namespace", namespace), attribute.String("namespace", namespace),
attribute.String("externalID", externalID.String()), attribute.String("name", name),
attribute.Int64("version", version),
)) ))
defer span.End() defer span.End()
start := time.Now() start := time.Now()
err := s.store.Delete(ctx, namespace, externalID.String()) err := s.store.Delete(ctx, namespace, name, version)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete encrypted value: %w", err) 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 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( ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes(
attribute.String("namespace", namespace), attribute.String("namespace", namespace),
attribute.String("externalID", externalID.String()), attribute.String("name", name),
attribute.Int64("version", version),
)) ))
defer span.End() 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) 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 { if err != nil {
return fmt.Errorf("failed to update encrypted value: %w", err) return fmt.Errorf("failed to update encrypted value: %w", err)
} }
@@ -1,23 +1,13 @@
package sqlkeeper package sqlkeeper_test
import ( import (
"context"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" 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/testutils"
"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" "github.com/grafana/grafana/pkg/tests/testsuite"
) )
@@ -26,159 +16,133 @@ func TestMain(m *testing.M) {
} }
func Test_SQLKeeperSetup(t *testing.T) { func Test_SQLKeeperSetup(t *testing.T) {
ctx := context.Background()
namespace1 := "namespace1" namespace1 := "namespace1"
name1 := "name1"
version1 := int64(1)
namespace2 := "namespace2" namespace2 := "namespace2"
name2 := "name2"
plaintext1 := "very secret string in namespace 1" plaintext1 := "very secret string in namespace 1"
plaintext2 := "very secret string in namespace 2" 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{} keeperCfg := &secretv1beta1.SystemKeeperConfig{}
t.Run("storing an encrypted value returns no error", func(t *testing.T) { t.Run("storing an encrypted value returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) sut := testutils.Setup(t)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
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.NoError(t, err)
require.NotEmpty(t, externalId2)
t.Run("expose the encrypted value from existing namespace", func(t *testing.T) { 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.NoError(t, err)
require.NotNil(t, exposedVal1) require.NotNil(t, exposedVal1)
assert.Equal(t, plaintext1, exposedVal1.DangerouslyExposeAndConsumeValue()) 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.NoError(t, err)
require.NotNil(t, exposedVal2) require.NotNil(t, exposedVal2)
assert.Equal(t, plaintext2, exposedVal2.DangerouslyExposeAndConsumeValue()) assert.Equal(t, plaintext2, exposedVal2.DangerouslyExposeAndConsumeValue())
}) })
t.Run("expose encrypted value from different namespace returns error", func(t *testing.T) { 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) require.Error(t, err)
assert.Empty(t, exposedVal) 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) require.Error(t, err)
assert.Empty(t, exposedVal) assert.Empty(t, exposedVal)
}) })
}) })
t.Run("storing same value in same namespace returns no error", func(t *testing.T) { t.Run("storing same value in same namespace returns error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) sut := testutils.Setup(t)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err) 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) { t.Run("storing same value in different namespace returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) sut := testutils.Setup(t)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext1) _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err) 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) { 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) require.Error(t, err)
assert.Empty(t, exposedVal) assert.Empty(t, exposedVal)
}) })
t.Run("deleting an existing encrypted value does not return error", func(t *testing.T) { t.Run("deleting an existing encrypted value does not return error", func(t *testing.T) {
externalID, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) sut := testutils.Setup(t)
require.NoError(t, err)
require.NotEmpty(t, externalID)
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) require.NoError(t, err)
assert.NotNil(t, exposedVal) assert.NotNil(t, exposedVal)
assert.Equal(t, plaintext1, exposedVal.DangerouslyExposeAndConsumeValue()) 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) require.NoError(t, err)
}) })
t.Run("deleting an non existing encrypted value does not return error", func(t *testing.T) { 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) require.NoError(t, err)
}) })
t.Run("updating an existent encrypted value returns no error", func(t *testing.T) { t.Run("updating an existent encrypted value returns no error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) sut := testutils.Setup(t)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
err = sqlKeeper.Update(ctx, keeperCfg, namespace1, externalId1, plaintext2) _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
require.NoError(t, err) 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) require.NoError(t, err)
assert.NotNil(t, exposedVal) assert.NotNil(t, exposedVal)
assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue()) assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue())
}) })
t.Run("updating a non existent encrypted value returns error", func(t *testing.T) { t.Run("updating a non existent encrypted value returns error", func(t *testing.T) {
externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) sut := testutils.Setup(t)
require.NoError(t, err)
require.NotEmpty(t, externalId1)
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) 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
}
@@ -60,7 +60,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv
defer span.End() defer span.End()
if newSecureValue.Spec.Value == nil { if newSecureValue.Spec.Value == nil {
decrypted, err := s.secureValueMetadataStorage.ReadForDecrypt(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name) currentVersion, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{})
if err != nil { if err != nil {
return nil, false, fmt.Errorf("reading secure value secret: %+w", err) return nil, false, fmt.Errorf("reading secure value secret: %+w", err)
} }
@@ -77,7 +77,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv
} }
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type()) logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type())
secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, contracts.ExternalID(decrypted.ExternalID)) secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, newSecureValue.Name, currentVersion.Status.Version)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("reading secret value from keeper: %w", err) return nil, false, fmt.Errorf("reading secret value from keeper: %w", err)
} }
@@ -100,31 +100,31 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1b
} }
// TODO: does this need to be for update? // TODO: does this need to be for update?
keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{ForUpdate: true}) keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, createdSv.Namespace, createdSv.Spec.Keeper, contracts.ReadOpts{ForUpdate: true})
if err != nil { if err != nil {
return nil, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err) return nil, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, createdSv.Spec.Keeper, err)
} }
keeper, err := s.keeperService.KeeperForConfig(keeperCfg) keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err) return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, createdSv.Spec.Keeper, err)
} }
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", sv.Namespace, "keeperName", sv.Spec.Keeper, "type", keeperCfg.Type()) logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "keeperName", createdSv.Spec.Keeper, "type", keeperCfg.Type())
// TODO: can we stop using external id? // TODO: can we stop using external id?
// TODO: store uses only the namespace and returns and id. It could be a kv instead. // TODO: store uses only the namespace and returns and id. It could be a kv instead.
// TODO: check that the encrypted store works with multiple versions // TODO: check that the encrypted store works with multiple versions
externalID, err := keeper.Store(ctx, keeperCfg, sv.Namespace, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) externalID, err := keeper.Store(ctx, keeperCfg, createdSv.Namespace, createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
if err != nil { if err != nil {
return nil, fmt.Errorf("storing secure value in keeper: %w", err) return nil, fmt.Errorf("storing secure value in keeper: %w", err)
} }
createdSv.Status.ExternalID = string(externalID) createdSv.Status.ExternalID = string(externalID)
if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(sv.Namespace), sv.Name, createdSv.Status.Version, externalID); err != nil { if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil {
return nil, fmt.Errorf("setting secure value external id: %w", err) return nil, fmt.Errorf("setting secure value external id: %w", err)
} }
if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(sv.Namespace), sv.Name, createdSv.Status.Version); err != nil { if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version); err != nil {
return nil, fmt.Errorf("marking secure value version as active: %w", err) return nil, fmt.Errorf("marking secure value version as active: %w", err)
} }
@@ -101,10 +101,10 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
require.NoError(t, err) require.NoError(t, err)
// Initialize encrypted value storage with a fake db // Initialize encrypted value storage with a fake db
encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features) encryptedValueStorage, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features)
require.NoError(t, err) require.NoError(t, err)
sqlKeeper := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encValueStore, nil) sqlKeeper := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, nil)
var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper)
@@ -124,9 +124,11 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
return Sut{ return Sut{
SecureValueService: secureValueService, SecureValueService: secureValueService,
SecureValueMetadataStorage: secureValueMetadataStorage, SecureValueMetadataStorage: secureValueMetadataStorage,
Database: database,
DecryptStorage: decryptStorage, DecryptStorage: decryptStorage,
DecryptService: decryptService, DecryptService: decryptService,
EncryptedValueStorage: encryptedValueStorage,
SQLKeeper: sqlKeeper,
Database: database,
} }
} }
@@ -135,6 +137,8 @@ type Sut struct {
SecureValueMetadataStorage contracts.SecureValueMetadataStorage SecureValueMetadataStorage contracts.SecureValueMetadataStorage
DecryptStorage contracts.DecryptStorage DecryptStorage contracts.DecryptStorage
DecryptService service.DecryptService DecryptService service.DecryptService
EncryptedValueStorage contracts.EncryptedValueStorage
SQLKeeper *sqlkeeper.SQLKeeper
Database *database.Database Database *database.Database
} }
@@ -1,12 +1,14 @@
INSERT INTO {{ .Ident "secret_encrypted_value" }} ( INSERT INTO {{ .Ident "secret_encrypted_value" }} (
{{ .Ident "uid" }},
{{ .Ident "namespace" }}, {{ .Ident "namespace" }},
{{ .Ident "name" }},
{{ .Ident "version" }},
{{ .Ident "encrypted_data" }}, {{ .Ident "encrypted_data" }},
{{ .Ident "created" }}, {{ .Ident "created" }},
{{ .Ident "updated" }} {{ .Ident "updated" }}
) VALUES ( ) VALUES (
{{ .Arg .Row.UID }},
{{ .Arg .Row.Namespace }}, {{ .Arg .Row.Namespace }},
{{ .Arg .Row.Name }},
{{ .Arg .Row.Version }},
{{ .Arg .Row.EncryptedData }}, {{ .Arg .Row.EncryptedData }},
{{ .Arg .Row.Created }}, {{ .Arg .Row.Created }},
{{ .Arg .Row.Updated }} {{ .Arg .Row.Updated }}
@@ -1,4 +1,6 @@
DELETE FROM {{ .Ident "secret_encrypted_value" }} DELETE FROM {{ .Ident "secret_encrypted_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND WHERE
{{ .Ident "uid" }} = {{ .Arg .UID }} {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }} AND
{{ .Ident "version" }} = {{ .Arg .Version }}
; ;
@@ -1,11 +1,14 @@
SELECT SELECT
{{ .Ident "uid" }},
{{ .Ident "namespace" }}, {{ .Ident "namespace" }},
{{ .Ident "name" }},
{{ .Ident "version" }},
{{ .Ident "encrypted_data" }}, {{ .Ident "encrypted_data" }},
{{ .Ident "created" }}, {{ .Ident "created" }},
{{ .Ident "updated" }} {{ .Ident "updated" }}
FROM FROM
{{ .Ident "secret_encrypted_value" }} {{ .Ident "secret_encrypted_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND WHERE
{{ .Ident "uid" }} = {{ .Arg .UID }} {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }} AND
{{ .Ident "version" }} = {{ .Arg .Version }}
; ;
@@ -3,6 +3,8 @@ UPDATE
SET SET
{{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }}, {{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }},
{{ .Ident "updated" }} = {{ .Arg .Updated }} {{ .Ident "updated" }} = {{ .Arg .Updated }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND WHERE
{{ .Ident "uid" }} = {{ .Arg .UID }} {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }} AND
{{ .Ident "version" }} = {{ .Arg .Version }}
; ;
@@ -3,8 +3,9 @@ package encryption
import "github.com/grafana/grafana/pkg/storage/secret/migrator" import "github.com/grafana/grafana/pkg/storage/secret/migrator"
type EncryptedValue struct { type EncryptedValue struct {
UID string
Namespace string Namespace string
Name string
Version int64
EncryptedData []byte EncryptedData []byte
Created int64 Created int64
Updated int64 Updated int64
@@ -6,17 +6,19 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/google/uuid"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
) )
var ( var (
ErrEncryptedValueNotFound = errors.New("encrypted value not found") ErrEncryptedValueNotFound = errors.New("encrypted value not found")
ErrEncryptedValueAlreadyExists = errors.New("encrypted value alredy exists")
ErrUnexpectedNumberOfRowsAffected = errors.New("unexpected number of rows modified by query")
) )
func ProvideEncryptedValueStorage( func ProvideEncryptedValueStorage(
@@ -42,7 +44,7 @@ type encryptedValStorage struct {
tracer trace.Tracer tracer trace.Tracer
} }
func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encryptedData []byte) (ev *contracts.EncryptedValue, err error) { func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (ev *contracts.EncryptedValue, err error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes(
attribute.String("namespace", namespace), attribute.String("namespace", namespace),
)) ))
@@ -50,14 +52,20 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encr
defer func() { defer func() {
if ev != nil { if ev != nil {
span.SetAttributes(attribute.String("uid", ev.UID)) span.SetAttributes(
attribute.String("namespace", ev.Namespace),
attribute.String("name", ev.Name),
attribute.Int64("version", ev.Version),
)
} }
}() }()
createdTime := time.Now().Unix() createdTime := time.Now().Unix()
encryptedValue := &EncryptedValue{ encryptedValue := &EncryptedValue{
UID: uuid.New().String(),
Namespace: namespace, Namespace: namespace,
Name: name,
Version: version,
EncryptedData: encryptedData, EncryptedData: encryptedData,
Created: createdTime, Created: createdTime,
Updated: createdTime, Updated: createdTime,
@@ -74,6 +82,9 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encr
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) res, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil { if err != nil {
if sql.IsRowAlreadyExistsError(err) {
return nil, ErrEncryptedValueAlreadyExists
}
return nil, fmt.Errorf("inserting row: %w", err) return nil, fmt.Errorf("inserting row: %w", err)
} }
@@ -84,25 +95,28 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encr
} }
return &contracts.EncryptedValue{ return &contracts.EncryptedValue{
UID: encryptedValue.UID,
Namespace: encryptedValue.Namespace, Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedData: encryptedValue.EncryptedData, EncryptedData: encryptedValue.EncryptedData,
Created: encryptedValue.Created, Created: encryptedValue.Created,
Updated: encryptedValue.Updated, Updated: encryptedValue.Updated,
}, nil }, nil
} }
func (s *encryptedValStorage) Update(ctx context.Context, namespace string, uid string, encryptedData []byte) error { func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes(
attribute.String("uid", uid),
attribute.String("namespace", namespace), attribute.String("namespace", namespace),
attribute.String("name", name),
attribute.Int64("version", version),
)) ))
defer span.End() defer span.End()
req := updateEncryptedValue{ req := updateEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect), SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace, Namespace: namespace,
UID: uid, Name: name,
Version: version,
EncryptedData: encryptedData, EncryptedData: encryptedData,
Updated: time.Now().Unix(), Updated: time.Now().Unix(),
} }
@@ -120,23 +134,25 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace string, uid
if rowsAffected, err := res.RowsAffected(); err != nil { if rowsAffected, err := res.RowsAffected(); err != nil {
return fmt.Errorf("getting rows affected: %w", err) return fmt.Errorf("getting rows affected: %w", err)
} else if rowsAffected != 1 { } else if rowsAffected != 1 {
return fmt.Errorf("expected 1 row affected, got %d on %s", rowsAffected, namespace) return fmt.Errorf("expected 1 row affected, got %d on %s: %w", rowsAffected, namespace, ErrUnexpectedNumberOfRowsAffected)
} }
return nil return nil
} }
func (s *encryptedValStorage) Get(ctx context.Context, namespace string, uid string) (*contracts.EncryptedValue, error) { func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, version int64) (*contracts.EncryptedValue, error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes(
attribute.String("uid", uid),
attribute.String("namespace", namespace), attribute.String("namespace", namespace),
attribute.String("name", name),
attribute.Int64("version", version),
)) ))
defer span.End() defer span.End()
req := &readEncryptedValue{ req := &readEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect), SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace, Namespace: namespace,
UID: uid, Name: name,
Version: version,
} }
query, err := sqltemplate.Execute(sqlEncryptedValueRead, req) query, err := sqltemplate.Execute(sqlEncryptedValueRead, req)
if err != nil { if err != nil {
@@ -154,7 +170,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace string, uid str
} }
var encryptedValue EncryptedValue var encryptedValue EncryptedValue
err = rows.Scan(&encryptedValue.UID, &encryptedValue.Namespace, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated) err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to scan encrypted value row: %w", err) return nil, fmt.Errorf("failed to scan encrypted value row: %w", err)
} }
@@ -163,25 +179,28 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace string, uid str
} }
return &contracts.EncryptedValue{ return &contracts.EncryptedValue{
UID: encryptedValue.UID,
Namespace: encryptedValue.Namespace, Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedData: encryptedValue.EncryptedData, EncryptedData: encryptedValue.EncryptedData,
Created: encryptedValue.Created, Created: encryptedValue.Created,
Updated: encryptedValue.Updated, Updated: encryptedValue.Updated,
}, nil }, nil
} }
func (s *encryptedValStorage) Delete(ctx context.Context, namespace string, uid string) error { func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string, version int64) error {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes(
attribute.String("uid", uid),
attribute.String("namespace", namespace), attribute.String("namespace", namespace),
attribute.String("name", name),
attribute.Int64("version", version),
)) ))
defer span.End() defer span.End()
req := deleteEncryptedValue{ req := deleteEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect), SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace, Namespace: namespace,
UID: uid, Name: name,
Version: version,
} }
query, err := sqltemplate.Execute(sqlEncryptedValueDelete, req) query, err := sqltemplate.Execute(sqlEncryptedValueDelete, req)
if err != nil { if err != nil {
@@ -1,46 +1,47 @@
package encryption package encryption_test
import ( import (
"context" "errors"
"slices"
"testing" "testing"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/encryption"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop" "pgregory.net/rapid"
) )
func TestEncryptedValueStoreImpl(t *testing.T) { func TestEncryptedValueStoreImpl(t *testing.T) {
// Initialize data key storage with a fake db t.Parallel()
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)
ctx := context.Background()
store, err := ProvideEncryptedValueStorage(database, tracer, features)
require.NoError(t, err)
t.Run("creating an encrypted value returns it", func(t *testing.T) { t.Run("creating an encrypted value returns it", func(t *testing.T) {
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data"))
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, createdEV.UID) require.NotEmpty(t, createdEV.Namespace)
require.NotEmpty(t, createdEV.Name)
require.NotEmpty(t, createdEV.Created) require.NotEmpty(t, createdEV.Created)
require.NotEmpty(t, createdEV.Updated) require.NotEmpty(t, createdEV.Updated)
require.NotEmpty(t, createdEV.EncryptedData) require.NotEmpty(t, createdEV.EncryptedData)
require.Equal(t, "test-namespace", createdEV.Namespace) require.Equal(t, "test-namespace", createdEV.Namespace)
require.Equal(t, "test-name", createdEV.Name)
}) })
t.Run("get an existent encrypted value returns it", func(t *testing.T) { t.Run("get an existent encrypted value returns it", func(t *testing.T) {
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data"))
require.NoError(t, err) require.NoError(t, err)
obtainedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, createdEV.UID, obtainedEV.UID) require.Equal(t, createdEV.Namespace, obtainedEV.Namespace)
require.Equal(t, createdEV.Name, obtainedEV.Name)
require.Equal(t, createdEV.Created, obtainedEV.Created) require.Equal(t, createdEV.Created, obtainedEV.Created)
require.Equal(t, createdEV.Updated, obtainedEV.Updated) require.Equal(t, createdEV.Updated, obtainedEV.Updated)
require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData) require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData)
@@ -48,10 +49,13 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
}) })
t.Run("get an existent encrypted value with a different namespace returns error", func(t *testing.T) { t.Run("get an existent encrypted value with a different namespace returns error", func(t *testing.T) {
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, []byte("test-data"))
require.NoError(t, err) require.NoError(t, err)
obtainedEV, err := store.Get(ctx, "other-test-namespace", createdEV.UID) obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "ns2", createdEV.Name, createdEV.Version)
require.Error(t, err) require.Error(t, err)
require.Equal(t, "encrypted value not found", err.Error()) require.Equal(t, "encrypted value not found", err.Error())
@@ -59,20 +63,26 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
}) })
t.Run("get a non existent encrypted value returns error", func(t *testing.T) { t.Run("get a non existent encrypted value returns error", func(t *testing.T) {
obtainedEV, err := store.Get(ctx, "test-namespace", "test-uid") t.Parallel()
sut := testutils.Setup(t)
obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "test-namespace", "test-name", 1)
require.Error(t, err) require.Error(t, err)
require.Equal(t, "encrypted value not found", err.Error()) require.Equal(t, "encrypted value not found", err.Error())
require.Nil(t, obtainedEV) require.Nil(t, obtainedEV)
}) })
t.Run("updating an existing encrypted value returns no error", func(t *testing.T) { t.Run("updating an existing encrypted value returns no error", func(t *testing.T) {
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data"))
require.NoError(t, err) require.NoError(t, err)
err = store.Update(ctx, "test-namespace", createdEV.UID, []byte("test-data-updated")) err = sut.EncryptedValueStorage.Update(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version, []byte("test-data-updated"))
require.NoError(t, err) require.NoError(t, err)
updatedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData) require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData)
@@ -81,27 +91,192 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
}) })
t.Run("updating a non existing encrypted value returns error", func(t *testing.T) { t.Run("updating a non existing encrypted value returns error", func(t *testing.T) {
err := store.Update(ctx, "test-namespace", "test-uid", []byte("test-data")) t.Parallel()
sut := testutils.Setup(t)
err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, []byte("test-data"))
require.Error(t, err) require.Error(t, err)
}) })
t.Run("delete an existing encrypted value returns error", func(t *testing.T) { t.Run("delete an existing encrypted value returns error", func(t *testing.T) {
createdEV, err := store.Create(ctx, "test-namespace", []byte("ttttest-data")) t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("ttttest-data"))
require.NoError(t, err) require.NoError(t, err)
obtainedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) _, err = sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
require.NoError(t, err) require.NoError(t, err)
err = store.Delete(ctx, "test-namespace", obtainedEV.UID) err = sut.EncryptedValueStorage.Delete(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
require.NoError(t, err) require.NoError(t, err)
obtainedEV, err = store.Get(ctx, "test-namespace", createdEV.UID) obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
require.Error(t, err) require.Error(t, err)
require.Nil(t, obtainedEV) require.Nil(t, obtainedEV)
}) })
t.Run("delete a non existing encrypted value does not return error", func(t *testing.T) { t.Run("delete a non existing encrypted value does not return error", func(t *testing.T) {
err := store.Delete(ctx, "test-namespace", "test-uid") t.Parallel()
sut := testutils.Setup(t)
err := sut.EncryptedValueStorage.Delete(t.Context(), "test-namespace", "test-name", 1)
require.NoError(t, err) require.NoError(t, err)
}) })
} }
func TestStateMachine(t *testing.T) {
t.Parallel()
tt := t
rapid.Check(t, func(t *rapid.T) {
sut := testutils.Setup(tt)
m := newModel()
t.Repeat(map[string]func(*rapid.T){
"create": func(t *rapid.T) {
ns := namespaceGen.Draw(t, "ns")
name := nameGen.Draw(t, "name")
version := versionGen.Draw(t, "version")
plaintext := rapid.String().Draw(t, "plaintext")
_, modelErr := m.create(ns, name, version, []byte(plaintext))
_, err := sut.EncryptedValueStorage.Create(t.Context(), ns, name, version, []byte(plaintext))
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
}
},
"update": func(t *rapid.T) {
ns := namespaceGen.Draw(t, "ns")
name := nameGen.Draw(t, "name")
version := versionGen.Draw(t, "version")
plaintext := rapid.String().Draw(t, "plaintext")
modelErr := m.update(ns, name, version, []byte(plaintext))
err := sut.EncryptedValueStorage.Update(t.Context(), ns, name, version, []byte(plaintext))
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
}
},
"get": func(t *rapid.T) {
ns := namespaceGen.Draw(t, "ns")
name := nameGen.Draw(t, "name")
version := versionGen.Draw(t, "version")
modelValue, modelErr := m.get(ns, name, version)
value, err := sut.EncryptedValueStorage.Get(t.Context(), ns, name, version)
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
}
// Do not compare timestamps because the model doesn't model them.
require.Equal(t, modelValue.Namespace, value.Namespace)
require.Equal(t, modelValue.Name, value.Name)
require.Equal(t, modelValue.EncryptedData, value.EncryptedData)
require.Equal(t, modelValue.Version, value.Version)
},
"delete": func(t *rapid.T) {
ns := namespaceGen.Draw(t, "ns")
name := nameGen.Draw(t, "name")
version := versionGen.Draw(t, "version")
modelErr := m.delete(ns, name, version)
err := sut.EncryptedValueStorage.Delete(t.Context(), ns, name, version)
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
}
},
})
})
}
var (
namespaceGen = rapid.Custom(func(t *rapid.T) string {
return rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}).Draw(t, "namespace")
})
nameGen = rapid.Custom(func(t *rapid.T) string {
return rapid.SampledFrom([]string{"name1", "name2", "name3", "name4", "name5"}).Draw(t, "name")
})
versionGen = rapid.Custom(func(t *rapid.T) int64 {
return rapid.Int64Range(1, 5).Draw(t, "version")
})
)
// A simplified model of the encrypted value storage
type model struct {
entries []*entry
}
type entry struct {
namespace string
name string
version int64
encryptedData []byte
}
func newModel() *model {
return &model{}
}
func (m *model) create(namespace, name string, version int64, encryptedData []byte) (*contracts.EncryptedValue, error) {
v, err := m.get(namespace, name, version)
if err != nil && !errors.Is(err, encryption.ErrEncryptedValueNotFound) {
return nil, err
}
// The entry being creted already exists
if v != nil {
return nil, encryption.ErrEncryptedValueAlreadyExists
}
m.entries = append(m.entries, &entry{
namespace: namespace,
name: name,
version: version,
encryptedData: encryptedData,
})
return &contracts.EncryptedValue{
Namespace: namespace,
Name: name,
Version: version,
EncryptedData: encryptedData,
Created: 1,
Updated: 1,
}, nil
}
func (m *model) update(namespace, name string, version int64, encryptedData []byte) error {
for _, v := range m.entries {
if v.namespace == namespace && v.name == name && v.version == version {
v.encryptedData = encryptedData
return nil
}
}
return encryption.ErrUnexpectedNumberOfRowsAffected
}
func (m *model) get(namespace, name string, version int64) (*contracts.EncryptedValue, error) {
for _, v := range m.entries {
if v.namespace == namespace && v.name == name && v.version == version {
return &contracts.EncryptedValue{
Namespace: namespace,
Name: name,
Version: version,
EncryptedData: v.encryptedData,
Created: 1,
Updated: 1,
}, nil
}
}
return nil, encryption.ErrEncryptedValueNotFound
}
func (m *model) delete(namespace, name string, version int64) error {
m.entries = slices.DeleteFunc(m.entries, func(v *entry) bool {
return v.namespace == namespace && v.name == name && v.version == version
})
return nil
}
+6 -3
View File
@@ -55,7 +55,8 @@ func (r createEncryptedValue) Validate() error {
type readEncryptedValue struct { type readEncryptedValue struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Namespace string
UID string Name string
Version int64
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` // Validate is only used if we use `dbutil` from `unifiedstorage`
@@ -67,7 +68,8 @@ func (r readEncryptedValue) Validate() error {
type updateEncryptedValue struct { type updateEncryptedValue struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Namespace string
UID string Name string
Version int64
EncryptedData []byte EncryptedData []byte
Updated int64 Updated int64
} }
@@ -81,7 +83,8 @@ func (r updateEncryptedValue) Validate() error {
type deleteEncryptedValue struct { type deleteEncryptedValue struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Namespace string
UID string Name string
Version int64
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` // Validate is only used if we use `dbutil` from `unifiedstorage`
+8 -4
View File
@@ -20,7 +20,8 @@ func TestEncryptedValueQueries(t *testing.T) {
SQLTemplate: mocks.NewTestingSQLTemplate(), SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &EncryptedValue{ Row: &EncryptedValue{
Namespace: "ns", Namespace: "ns",
UID: "abc123", Name: "n1",
Version: 1,
EncryptedData: []byte("secret"), EncryptedData: []byte("secret"),
Created: 1234, Created: 1234,
Updated: 5678, Updated: 5678,
@@ -34,7 +35,8 @@ func TestEncryptedValueQueries(t *testing.T) {
Data: &readEncryptedValue{ Data: &readEncryptedValue{
SQLTemplate: mocks.NewTestingSQLTemplate(), SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns", Namespace: "ns",
UID: "abc123", Name: "n1",
Version: 1,
}, },
}, },
}, },
@@ -44,7 +46,8 @@ func TestEncryptedValueQueries(t *testing.T) {
Data: &updateEncryptedValue{ Data: &updateEncryptedValue{
SQLTemplate: mocks.NewTestingSQLTemplate(), SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns", Namespace: "ns",
UID: "abc123", Name: "n1",
Version: 1,
EncryptedData: []byte("secret"), EncryptedData: []byte("secret"),
Updated: 5679, Updated: 5679,
}, },
@@ -56,7 +59,8 @@ func TestEncryptedValueQueries(t *testing.T) {
Data: &deleteEncryptedValue{ Data: &deleteEncryptedValue{
SQLTemplate: mocks.NewTestingSQLTemplate(), SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns", Namespace: "ns",
UID: "abc123", Name: "n1",
Version: 1,
}, },
}, },
}, },
@@ -1,12 +1,14 @@
INSERT INTO `secret_encrypted_value` ( INSERT INTO `secret_encrypted_value` (
`uid`,
`namespace`, `namespace`,
`name`,
`version`,
`encrypted_data`, `encrypted_data`,
`created`, `created`,
`updated` `updated`
) VALUES ( ) VALUES (
'abc123',
'ns', 'ns',
'n1',
1,
'[115 101 99 114 101 116]', '[115 101 99 114 101 116]',
1234, 1234,
5678 5678
@@ -1,4 +1,6 @@
DELETE FROM `secret_encrypted_value` DELETE FROM `secret_encrypted_value`
WHERE `namespace` = 'ns' AND WHERE
`uid` = 'abc123' `namespace` = 'ns' AND
`name` = 'n1' AND
`version` = 1
; ;
@@ -1,11 +1,14 @@
SELECT SELECT
`uid`,
`namespace`, `namespace`,
`name`,
`version`,
`encrypted_data`, `encrypted_data`,
`created`, `created`,
`updated` `updated`
FROM FROM
`secret_encrypted_value` `secret_encrypted_value`
WHERE `namespace` = 'ns' AND WHERE
`uid` = 'abc123' `namespace` = 'ns' AND
`name` = 'n1' AND
`version` = 1
; ;
@@ -3,6 +3,8 @@ UPDATE
SET SET
`encrypted_data` = '[115 101 99 114 101 116]', `encrypted_data` = '[115 101 99 114 101 116]',
`updated` = 5679 `updated` = 5679
WHERE `namespace` = 'ns' AND WHERE
`uid` = 'abc123' `namespace` = 'ns' AND
`name` = 'n1' AND
`version` = 1
; ;
@@ -1,12 +1,14 @@
INSERT INTO "secret_encrypted_value" ( INSERT INTO "secret_encrypted_value" (
"uid",
"namespace", "namespace",
"name",
"version",
"encrypted_data", "encrypted_data",
"created", "created",
"updated" "updated"
) VALUES ( ) VALUES (
'abc123',
'ns', 'ns',
'n1',
1,
'[115 101 99 114 101 116]', '[115 101 99 114 101 116]',
1234, 1234,
5678 5678
@@ -1,4 +1,6 @@
DELETE FROM "secret_encrypted_value" DELETE FROM "secret_encrypted_value"
WHERE "namespace" = 'ns' AND WHERE
"uid" = 'abc123' "namespace" = 'ns' AND
"name" = 'n1' AND
"version" = 1
; ;
@@ -1,11 +1,14 @@
SELECT SELECT
"uid",
"namespace", "namespace",
"name",
"version",
"encrypted_data", "encrypted_data",
"created", "created",
"updated" "updated"
FROM FROM
"secret_encrypted_value" "secret_encrypted_value"
WHERE "namespace" = 'ns' AND WHERE
"uid" = 'abc123' "namespace" = 'ns' AND
"name" = 'n1' AND
"version" = 1
; ;
@@ -3,6 +3,8 @@ UPDATE
SET SET
"encrypted_data" = '[115 101 99 114 101 116]', "encrypted_data" = '[115 101 99 114 101 116]',
"updated" = 5679 "updated" = 5679
WHERE "namespace" = 'ns' AND WHERE
"uid" = 'abc123' "namespace" = 'ns' AND
"name" = 'n1' AND
"version" = 1
; ;
@@ -1,12 +1,14 @@
INSERT INTO "secret_encrypted_value" ( INSERT INTO "secret_encrypted_value" (
"uid",
"namespace", "namespace",
"name",
"version",
"encrypted_data", "encrypted_data",
"created", "created",
"updated" "updated"
) VALUES ( ) VALUES (
'abc123',
'ns', 'ns',
'n1',
1,
'[115 101 99 114 101 116]', '[115 101 99 114 101 116]',
1234, 1234,
5678 5678
@@ -1,4 +1,6 @@
DELETE FROM "secret_encrypted_value" DELETE FROM "secret_encrypted_value"
WHERE "namespace" = 'ns' AND WHERE
"uid" = 'abc123' "namespace" = 'ns' AND
"name" = 'n1' AND
"version" = 1
; ;
@@ -1,11 +1,14 @@
SELECT SELECT
"uid",
"namespace", "namespace",
"name",
"version",
"encrypted_data", "encrypted_data",
"created", "created",
"updated" "updated"
FROM FROM
"secret_encrypted_value" "secret_encrypted_value"
WHERE "namespace" = 'ns' AND WHERE
"uid" = 'abc123' "namespace" = 'ns' AND
"name" = 'n1' AND
"version" = 1
; ;
@@ -3,6 +3,8 @@ UPDATE
SET SET
"encrypted_data" = '[115 101 99 114 101 116]', "encrypted_data" = '[115 101 99 114 101 116]',
"updated" = 5679 "updated" = 5679
WHERE "namespace" = 'ns' AND WHERE
"uid" = 'abc123' "namespace" = 'ns' AND
"name" = 'n1' AND
"version" = 1
; ;
+4 -4
View File
@@ -94,17 +94,17 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace,
// The auth token will not necessarily have the permission to read the secure value metadata, // The auth token will not necessarily have the permission to read the secure value metadata,
// but we still need to do it to inspect the `decrypters` field, hence the actual `authorize` // but we still need to do it to inspect the `decrypters` field, hence the actual `authorize`
// function call happens after this. // function call happens after this.
sv, err := s.secureValueMetadataStorage.ReadForDecrypt(ctx, namespace, name) sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{})
if err != nil { if err != nil {
return "", contracts.ErrDecryptNotFound return "", contracts.ErrDecryptNotFound
} }
decrypterIdentity, authorized := s.decryptAuthorizer.Authorize(ctx, name, sv.Decrypters) decrypterIdentity, authorized := s.decryptAuthorizer.Authorize(ctx, name, sv.Spec.Decrypters)
if !authorized { if !authorized {
return "", contracts.ErrDecryptNotAuthorized return "", contracts.ErrDecryptNotAuthorized
} }
keeperConfig, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, namespace.String(), sv.Keeper, contracts.ReadOpts{}) keeperConfig, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, namespace.String(), sv.Spec.Keeper, contracts.ReadOpts{})
if err != nil { if err != nil {
return "", contracts.ErrDecryptFailed return "", contracts.ErrDecryptFailed
} }
@@ -114,7 +114,7 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace,
return "", contracts.ErrDecryptFailed return "", contracts.ErrDecryptFailed
} }
exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace.String(), contracts.ExternalID(sv.ExternalID)) exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace.String(), name, sv.Status.Version)
if err != nil { if err != nil {
return "", contracts.ErrDecryptFailed return "", contracts.ErrDecryptFailed
} }
@@ -197,6 +197,7 @@ func (s *secureValueMetadataStorage) getLatestVersion(ctx context.Context, names
return &version, nil return &version, nil
} }
// TODO: can this method + queries be removed?
func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) { func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) {
start := time.Now() start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.ReadForDecrypt", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.ReadForDecrypt", trace.WithAttributes(
+8 -4
View File
@@ -118,17 +118,21 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) {
Indices: []*migrator.Index{}, // TODO: add indexes based on the queries we make. Indices: []*migrator.Index{}, // TODO: add indexes based on the queries we make.
}) })
tables = append(tables, migrator.Table{ encryptedValueTable := migrator.Table{
Name: TableNameEncryptedValue, Name: TableNameEncryptedValue,
Columns: []*migrator.Column{ Columns: []*migrator.Column{
{Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s. {Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "uid", Type: migrator.DB_NVarchar, Length: 36, IsPrimaryKey: true}, // Fixed size of a UUID. {Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false},
{Name: "version", Type: migrator.DB_BigInt, Nullable: false},
{Name: "encrypted_data", Type: migrator.DB_Blob, Nullable: false}, {Name: "encrypted_data", Type: migrator.DB_Blob, Nullable: false},
{Name: "created", Type: migrator.DB_BigInt, Nullable: false}, {Name: "created", Type: migrator.DB_BigInt, Nullable: false},
{Name: "updated", Type: migrator.DB_BigInt, Nullable: false}, {Name: "updated", Type: migrator.DB_BigInt, Nullable: false},
}, },
Indices: []*migrator.Index{}, // TODO: add indexes based on the queries we make. Indices: []*migrator.Index{
}) {Cols: []string{"namespace", "name", "version"}, Type: migrator.UniqueIndex},
},
}
tables = append(tables, encryptedValueTable)
// Initialize all tables // Initialize all tables
for t := range tables { for t := range tables {