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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
;
|
;
|
||||||
|
|||||||
+6
-3
@@ -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
|
||||||
;
|
;
|
||||||
|
|||||||
+4
-2
@@ -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
-2
@@ -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
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
;
|
;
|
||||||
|
|||||||
+6
-3
@@ -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
|
||||||
;
|
;
|
||||||
|
|||||||
+4
-2
@@ -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
-2
@@ -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
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
;
|
;
|
||||||
|
|||||||
+6
-3
@@ -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
|
||||||
;
|
;
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user