diff --git a/conf/defaults.ini b/conf/defaults.ini index dd3b6cd1d3c..39d79f1509e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -2146,6 +2146,15 @@ frontend_poll_interval = 2s # With "unchanged", all Alert Rules will be created with the pause state unchanged coming from the source instance. alert_rules_state = "paused" +###################################### Secrets Manager ###################################### +[secrets_manager] +# Used for signing +secret_key = SW2YcwTIb9zpOOhoPsMm +# Current key provider used for envelope encryption, default to static value specified by secret_key +encryption_provider = secretKey.v1 +# List of configured key providers, space separated (Enterprise only): e.g., awskms.v1 azurekv.v1 +available_encryption_providers = + ################################## Frontend development configuration ################################### # Warning! Any settings placed in this section will be available on `process.env.frontend_dev_{foo}` within frontend code # Any values placed here may be accessible to the UI. Do not place sensitive information here. diff --git a/conf/sample.ini b/conf/sample.ini index 892e294492d..d4f71ccdb51 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -2047,6 +2047,15 @@ default_datasource_uid = # With "unchanged", all Alert Rules will be created with the pause state unchanged coming from the source instance. ;alert_rules_state = "paused" +###################################### Secrets Manager ###################################### +[secrets_manager] +# Used for signing +;secret_key = SW2YcwTIb9zpOOhoPsMm +# Current key provider used for envelope encryption, default to static value specified by secret_key +;encryption_provider = secretKey.v1 +# List of configured key providers, space separated (Enterprise only): e.g., awskms.v1 azurekv.v1 +;available_encryption_providers = + ################################## Frontend development configuration ################################### # Warning! Any settings placed in this section will be available on `process.env.frontend_dev_{foo}` within frontend code # Any values placed here may be accessible to the UI. Do not place sensitive information here. diff --git a/pkg/registry/apis/secret/contracts/encryption.go b/pkg/registry/apis/secret/contracts/encryption.go index 915674179c0..176fb8ace24 100644 --- a/pkg/registry/apis/secret/contracts/encryption.go +++ b/pkg/registry/apis/secret/contracts/encryption.go @@ -10,9 +10,6 @@ type EncryptionManager interface { // implementation present at manager.EncryptionService. Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) - - RotateDataKeys(ctx context.Context, namespace string) error - ReEncryptDataKeys(ctx context.Context, namespace string) error } type EncryptedValue struct { diff --git a/pkg/registry/apis/secret/encryption/cipher/cipher.go b/pkg/registry/apis/secret/encryption/cipher/cipher.go index 73a8b245d54..15f724f5557 100644 --- a/pkg/registry/apis/secret/encryption/cipher/cipher.go +++ b/pkg/registry/apis/secret/encryption/cipher/cipher.go @@ -4,11 +4,6 @@ import ( "context" ) -const ( - AesCfb = "aes-cfb" - AesGcm = "aes-gcm" -) - type Cipher interface { Encrypter Decrypter @@ -21,8 +16,3 @@ type Encrypter interface { type Decrypter interface { Decrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) } - -type Provider interface { - ProvideCiphers() map[string]Encrypter - ProvideDeciphers() map[string]Decrypter -} diff --git a/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm.go b/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm.go index c5072b60092..39b28aabdb6 100644 --- a/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm.go +++ b/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm.go @@ -10,7 +10,10 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" ) -const gcmSaltLength = 8 +const ( + gcmSaltLength = 8 + AesGcm = "aes-gcm" +) var ( _ cipher.Encrypter = (*aesGcmCipher)(nil) @@ -23,7 +26,7 @@ type aesGcmCipher struct { randReader io.Reader } -func newAesGcmCipher() aesGcmCipher { +func NewAesGcmCipher() aesGcmCipher { return aesGcmCipher{ randReader: rand.Reader, } diff --git a/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm_test.go b/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm_test.go index c09c66d9b9e..09b485c2482 100644 --- a/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm_test.go +++ b/pkg/registry/apis/secret/encryption/cipher/provider/cipher_aesgcm_test.go @@ -20,7 +20,7 @@ func TestGcmEncryption(t *testing.T) { salt := []byte("abcdefgh") nonce := []byte("123456789012") - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader(append(salt, nonce...)) payload := []byte("grafana unit test") @@ -40,7 +40,7 @@ func TestGcmEncryption(t *testing.T) { t.Run("fails if random source is empty", func(t *testing.T) { t.Parallel() - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader([]byte{}) payload := []byte("grafana unit test") @@ -56,7 +56,7 @@ func TestGcmEncryption(t *testing.T) { // Scenario: the random source has enough entropy for the salt, but not for the nonce. // In this case, we should fail with an error. - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader([]byte("abcdefgh")) // 8 bytes for salt, but not enough for nonce payload := []byte("grafana unit test") @@ -75,7 +75,7 @@ func TestGcmDecryption(t *testing.T) { // The expected values are generated by test_fixtures/aesgcm_encrypt_correct_output.rb - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader([]byte{}) // should not be used payload, err := hex.DecodeString("61626364656667683132333435363738393031328123655291d1f5eebe34c54ba55900f68a2700818a8fda9e2921190b67271d97ce") @@ -90,7 +90,7 @@ func TestGcmDecryption(t *testing.T) { t.Run("fails if payload is shorter than salt", func(t *testing.T) { t.Parallel() - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader([]byte{}) // should not be used payload := []byte{1, 2, 3, 4} @@ -103,7 +103,7 @@ func TestGcmDecryption(t *testing.T) { t.Run("fails if payload has length of salt but no nonce", func(t *testing.T) { t.Parallel() - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader([]byte{}) // should not be used payload := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // salt and a little more @@ -116,7 +116,7 @@ func TestGcmDecryption(t *testing.T) { t.Run("fails when authentication tag is wrong", func(t *testing.T) { t.Parallel() - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader([]byte{}) // should not be used // Removed 2 bytes from the end of the payload to simulate a wrong authentication tag. @@ -131,7 +131,7 @@ func TestGcmDecryption(t *testing.T) { t.Run("fails if secret does not match", func(t *testing.T) { t.Parallel() - cipher := newAesGcmCipher() + cipher := NewAesGcmCipher() cipher.randReader = bytes.NewReader([]byte{}) // should not be used payload, err := hex.DecodeString("61626364656667683132333435363738393031328123655291d1f5eebe34c54ba55900f68a2700818a8fda9e2921190b67271d97ce") diff --git a/pkg/registry/apis/secret/encryption/cipher/provider/decipher_aescfb.go b/pkg/registry/apis/secret/encryption/cipher/provider/decipher_aescfb.go deleted file mode 100644 index 8d1c77a2c82..00000000000 --- a/pkg/registry/apis/secret/encryption/cipher/provider/decipher_aescfb.go +++ /dev/null @@ -1,52 +0,0 @@ -package provider - -import ( - "context" - "crypto/aes" - cpr "crypto/cipher" - - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" -) - -const cfbSaltLength = 8 - -var _ cipher.Decrypter = aesCfbDecipher{} - -type aesCfbDecipher struct{} - -func (aesCfbDecipher) Decrypt(_ context.Context, payload []byte, secret string) ([]byte, error) { - // payload is formatted: - // Salt Nonce Encrypted - // | | Payload - // | | | - // | +---------v-------------+ | - // +-->SSSSSSSNNNNNNNEEEEEEEEE<--+ - // +-----------------------+ - - if len(payload) < cfbSaltLength+aes.BlockSize { - // If we don't return here, we'd panic. - return nil, ErrPayloadTooShort - } - - salt := payload[:cfbSaltLength] - - key, err := aes256CipherKey(secret, salt) - if err != nil { - return nil, err - } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - iv, payload := payload[cfbSaltLength:][:aes.BlockSize], payload[cfbSaltLength+aes.BlockSize:] - payloadDst := make([]byte, len(payload)) - - //nolint:staticcheck // We need to support CFB _decryption_, though we don't support it for future encryption. - stream := cpr.NewCFBDecrypter(block, iv) - - // XORKeyStream can work in-place if the two arguments are the same. - stream.XORKeyStream(payloadDst, payload) - return payloadDst, nil -} diff --git a/pkg/registry/apis/secret/encryption/cipher/provider/decipher_aescfb_test.go b/pkg/registry/apis/secret/encryption/cipher/provider/decipher_aescfb_test.go deleted file mode 100644 index c6e8f0f58df..00000000000 --- a/pkg/registry/apis/secret/encryption/cipher/provider/decipher_aescfb_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package provider - -import ( - "encoding/hex" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestCfbDecryption(t *testing.T) { - t.Parallel() - - t.Run("decrypts correctly", func(t *testing.T) { - t.Parallel() - - // The expected values are generated by test_fixtures/aescfb_encrypt_correct_output.rb - - cipher := aesCfbDecipher{} - - payload, err := hex.DecodeString("616263646566676831323334353637383930313234353637f1114227cb6af678cad6ee35f67f25f40b") - require.NoError(t, err, "failed to decode hex string") - secret := "secret here" - - decrypted, err := cipher.Decrypt(t.Context(), payload, secret) - require.NoError(t, err, "failed to decrypt with CFB") - require.Equal(t, "grafana unit test", string(decrypted), "decrypted payload should match expected value") - }) - - t.Run("fails if payload is too short", func(t *testing.T) { - t.Parallel() - - cipher := aesCfbDecipher{} - - payload := []byte{1, 2, 3, 4} - secret := "secret here" - - _, err := cipher.Decrypt(t.Context(), payload, secret) - require.Error(t, err, "expected error when payload is shorter than salt") - }) - - t.Run("fails if payload is not an AES-encrypted value", func(t *testing.T) { - t.Parallel() - - cipher := aesCfbDecipher{} - - payload, err := hex.DecodeString("616263646566676831323334353637383930313234353637f1114227cb") - require.NoError(t, err, "failed to decode hex string") - secret := "secret here" - - // We don't have any authentication tag, so we can't return an error in this case. - decrypted, err := cipher.Decrypt(t.Context(), payload, secret) - require.NoError(t, err, "expected no error") - require.NotEqual(t, "grafana unit test", string(decrypted), "decrypted payload should not match real exposed secret") - }) - - t.Run("fails if secret is wrong", func(t *testing.T) { - t.Parallel() - - cipher := aesCfbDecipher{} - - payload, err := hex.DecodeString("616263646566676831323334353637383930313234353637f1114227cb6af678cad6ee35f67f25f40b") - require.NoError(t, err, "failed to decode hex string") - secret := "should've been 'secret here'" - - // We don't have any authentication tag, so we can't return an error in this case. - decrypted, err := cipher.Decrypt(t.Context(), payload, secret) - require.NoError(t, err, "expected no error") - require.NotEqual(t, "grafana unit test", string(decrypted), "decrypted payload should not match real exposed secret") - }) -} diff --git a/pkg/registry/apis/secret/encryption/cipher/provider/provider.go b/pkg/registry/apis/secret/encryption/cipher/provider/provider.go deleted file mode 100644 index a299b5d20e1..00000000000 --- a/pkg/registry/apis/secret/encryption/cipher/provider/provider.go +++ /dev/null @@ -1,18 +0,0 @@ -package provider - -import ( - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" -) - -func ProvideCiphers() map[string]cipher.Encrypter { - return map[string]cipher.Encrypter{ - cipher.AesGcm: newAesGcmCipher(), - } -} - -func ProvideDeciphers() map[string]cipher.Decrypter { - return map[string]cipher.Decrypter{ - cipher.AesGcm: newAesGcmCipher(), - cipher.AesCfb: aesCfbDecipher{}, - } -} diff --git a/pkg/registry/apis/secret/encryption/cipher/provider/provider_test.go b/pkg/registry/apis/secret/encryption/cipher/provider/provider_test.go deleted file mode 100644 index b3977b74dc6..00000000000 --- a/pkg/registry/apis/secret/encryption/cipher/provider/provider_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package provider_test - -import ( - "testing" - - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/provider" - "github.com/stretchr/testify/require" -) - -func TestNoCfbEncryptionCipher(t *testing.T) { - // CFB encryption is insecure, and as such we should not permit any cipher for encryption to be added. - // Changing/removing this test MUST be accompanied with an approval from the app security team. - - ciphers := provider.ProvideCiphers() - require.NotContains(t, ciphers, cipher.AesCfb, "CFB cipher should not be used for encryption") -} diff --git a/pkg/registry/apis/secret/encryption/cipher/provider/test_fixtures/aescfb_encrypt_correct_output.rb b/pkg/registry/apis/secret/encryption/cipher/provider/test_fixtures/aescfb_encrypt_correct_output.rb deleted file mode 100644 index 002a1d3d1f1..00000000000 --- a/pkg/registry/apis/secret/encryption/cipher/provider/test_fixtures/aescfb_encrypt_correct_output.rb +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env ruby -# Used by ../decipher_aescfb_test.go -# Why Ruby? It has a mostly available OpenSSL library that can be easily fetched (and most who have Ruby already have it!). And it is easy to read for this purpose. - -require 'openssl' - -salt = "abcdefgh" -nonce = "1234567890124567" - -secret = "secret here" -plaintext = "grafana unit test" - -# reimpl of aes256CipherKey -# the key is always the same value given the inputs -iterations = 10_000 -len = 32 -hash = OpenSSL::Digest::SHA256.new -key = OpenSSL::KDF.pbkdf2_hmac(secret, salt: salt, iterations: iterations, length: len, hash: hash) - -cipher = OpenSSL::Cipher::AES256.new(:CFB).encrypt -cipher.iv = nonce -cipher.key = key -encrypted = cipher.update(plaintext) - -def to_hex(s) - s.unpack('H*').first -end - -# Salt Nonce Encrypted -# | | Payload -# | | | -# | +---------v-------------+ | -# +-->SSSSSSSNNNNNNNEEEEEEEEE<--+ -# +-----------------------+ -printf("%s%s%s%s\n", to_hex(salt), to_hex(nonce), cipher.final, to_hex(encrypted)) diff --git a/pkg/registry/apis/secret/encryption/cipher/service/service.go b/pkg/registry/apis/secret/encryption/cipher/service/service.go index 9bdf0cc714f..83d140da1a9 100644 --- a/pkg/registry/apis/secret/encryption/cipher/service/service.go +++ b/pkg/registry/apis/secret/encryption/cipher/service/service.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" - encryptionprovider "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/provider" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/provider" "github.com/grafana/grafana/pkg/setting" ) @@ -30,8 +30,9 @@ type Service struct { cfg *setting.Cfg usageMetrics usagestats.Service - ciphers map[string]cipher.Encrypter - deciphers map[string]cipher.Decrypter + cipher cipher.Encrypter + decipher cipher.Decrypter + algorithm string } func NewEncryptionService( @@ -43,59 +44,29 @@ func NewEncryptionService( return nil, fmt.Errorf("`[secrets_manager]secret_key` is not set") } - if cfg.SecretsManagement.Encryption.Algorithm == "" { - return nil, fmt.Errorf("`[secrets_manager.encryption]algorithm` is not set") - } - s := &Service{ tracer: tracer, log: log.New("encryption"), - ciphers: encryptionprovider.ProvideCiphers(), - deciphers: encryptionprovider.ProvideDeciphers(), + // Use the AES-GCM cipher for encryption and decryption. + // This is the only cipher supported by the secrets management system. + cipher: provider.NewAesGcmCipher(), + decipher: provider.NewAesGcmCipher(), + algorithm: provider.AesGcm, usageMetrics: usageMetrics, cfg: cfg, } - algorithm := s.cfg.SecretsManagement.Encryption.Algorithm - - if err := s.checkEncryptionAlgorithm(algorithm); err != nil { - return nil, err - } - s.registerUsageMetrics() return s, nil } -func (s *Service) checkEncryptionAlgorithm(algorithm string) error { - var err error - defer func() { - if err != nil { - s.log.Error("Wrong security encryption configuration", "algorithm", algorithm, "error", err) - } - }() - - if _, ok := s.ciphers[algorithm]; !ok { - err = fmt.Errorf("no cipher registered for encryption algorithm '%s'", algorithm) - return err - } - - if _, ok := s.deciphers[algorithm]; !ok { - err = fmt.Errorf("no decipher registered for encryption algorithm '%s'", algorithm) - return err - } - - return nil -} - func (s *Service) registerUsageMetrics() { s.usageMetrics.RegisterMetricsFunc(func(context.Context) (map[string]any, error) { - algorithm := s.cfg.SecretsManagement.Encryption.Algorithm - return map[string]any{ - fmt.Sprintf("stats.%s.encryption.cipher.%s.count", encryption.UsageInsightsPrefix, algorithm): 1, + fmt.Sprintf("stats.%s.encryption.cipher.%s.count", encryption.UsageInsightsPrefix, s.algorithm): 1, }, nil }) } @@ -120,16 +91,10 @@ func (s *Service) Decrypt(ctx context.Context, payload []byte, secret string) ([ return nil, err } - decipher, ok := s.deciphers[algorithm] - if !ok { - err = fmt.Errorf("no decipher available for algorithm '%s'", algorithm) - return nil, err - } - span.SetAttributes(attribute.String("cipher.algorithm", algorithm)) var decrypted []byte - decrypted, err = decipher.Decrypt(ctx, toDecrypt, secret) + decrypted, err = s.decipher.Decrypt(ctx, toDecrypt, secret) return decrypted, err } @@ -139,15 +104,8 @@ func (s *Service) deriveEncryptionAlgorithm(payload []byte) (string, []byte, err return "", nil, fmt.Errorf("unable to derive encryption algorithm") } - if payload[0] != encryptionAlgorithmDelimiter { - return cipher.AesCfb, payload, nil // backwards compatibility - } - payload = payload[1:] algorithmDelimiterIdx := bytes.Index(payload, []byte{encryptionAlgorithmDelimiter}) - if algorithmDelimiterIdx == -1 { - return cipher.AesCfb, payload, nil // backwards compatibility - } algorithmB64 := payload[:algorithmDelimiterIdx] payload = payload[algorithmDelimiterIdx+1:] @@ -173,21 +131,13 @@ func (s *Service) Encrypt(ctx context.Context, payload []byte, secret string) ([ } }() - algorithm := s.cfg.SecretsManagement.Encryption.Algorithm - - cipher, ok := s.ciphers[algorithm] - if !ok { - err = fmt.Errorf("no cipher available for algorithm '%s'", algorithm) - return nil, err - } - - span.SetAttributes(attribute.String("cipher.algorithm", algorithm)) + span.SetAttributes(attribute.String("cipher.algorithm", s.algorithm)) var encrypted []byte - encrypted, err = cipher.Encrypt(ctx, payload, secret) + encrypted, err = s.cipher.Encrypt(ctx, payload, secret) - prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len([]byte(algorithm)))+2) - base64.RawStdEncoding.Encode(prefix[1:], []byte(algorithm)) + prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len([]byte(s.algorithm)))+2) + base64.RawStdEncoding.Encode(prefix[1:], []byte(s.algorithm)) prefix[0] = encryptionAlgorithmDelimiter prefix[len(prefix)-1] = encryptionAlgorithmDelimiter diff --git a/pkg/registry/apis/secret/encryption/cipher/service/service_test.go b/pkg/registry/apis/secret/encryption/cipher/service/service_test.go index 7a207797ce8..62a65ea0dbc 100644 --- a/pkg/registry/apis/secret/encryption/cipher/service/service_test.go +++ b/pkg/registry/apis/secret/encryption/cipher/service/service_test.go @@ -2,14 +2,12 @@ package service import ( "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" "github.com/grafana/grafana/pkg/setting" ) @@ -21,11 +19,6 @@ func newGcmService(t *testing.T) *Service { SecretsManagement: setting.SecretsManagerSettings{ SecretKey: "SdlklWklckeLS", EncryptionProvider: "secretKey.v1", - Encryption: setting.EncryptionSettings{ - DataKeysCacheTTL: 5 * time.Minute, - DataKeysCleanupInterval: 1 * time.Nanosecond, - Algorithm: cipher.AesGcm, - }, }, } @@ -60,19 +53,4 @@ func TestService(t *testing.T) { assert.Equal(t, []byte("grafana"), decrypted) // We'll let the provider deal with testing details. }) - - t.Run("decrypting legacy ciphertext should work", func(t *testing.T) { - t.Parallel() - - // Raw slice of bytes that corresponds to the following ciphertext: - // - 'grafana' as payload - // - '1234' as secret - // - no encryption algorithm metadata - ciphertext := []byte{73, 71, 50, 57, 121, 110, 90, 109, 115, 23, 237, 13, 130, 188, 151, 118, 98, 103, 80, 209, 79, 143, 22, 122, 44, 40, 102, 41, 136, 16, 27} - - svc := newGcmService(t) - decrypted, err := svc.Decrypt(t.Context(), ciphertext, "1234") - require.NoError(t, err) - assert.Equal(t, []byte("grafana"), decrypted) - }) } diff --git a/pkg/registry/apis/secret/encryption/doc.go b/pkg/registry/apis/secret/encryption/doc.go new file mode 100644 index 00000000000..a4ca2a0fd2c --- /dev/null +++ b/pkg/registry/apis/secret/encryption/doc.go @@ -0,0 +1,5 @@ +// Package encryption provides envelope encryption for secrets manager + +// It is heavily copied from the legacy envelope encryption implementation at github.com/grafana/grafana/pkg/services/encryption. + +package encryption diff --git a/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider/grafana_provider.go b/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider/grafana_provider.go new file mode 100644 index 00000000000..067d455bfb6 --- /dev/null +++ b/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider/grafana_provider.go @@ -0,0 +1,28 @@ +package defaultprovider + +import ( + "context" + + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" +) + +type grafanaProvider struct { + sk string + encryption cipher.Cipher +} + +func New(sk string, encryption cipher.Cipher) encryption.Provider { + return grafanaProvider{ + sk: sk, + encryption: encryption, + } +} + +func (p grafanaProvider) Encrypt(ctx context.Context, blob []byte) ([]byte, error) { + return p.encryption.Encrypt(ctx, blob, p.sk) +} + +func (p grafanaProvider) Decrypt(ctx context.Context, blob []byte) ([]byte, error) { + return p.encryption.Decrypt(ctx, blob, p.sk) +} diff --git a/pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go b/pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go new file mode 100644 index 00000000000..0797d784c8e --- /dev/null +++ b/pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go @@ -0,0 +1,19 @@ +package kmsproviders + +import ( + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider" + "github.com/grafana/grafana/pkg/setting" +) + +const ( + // Default is the identifier of the default kms provider which fallbacks to the configured secret_key + Default = "secretKey.v1" +) + +func GetOSSKMSProviders(cfg *setting.Cfg, enc cipher.Cipher) encryption.ProviderMap { + return encryption.ProviderMap{ + Default: defaultprovider.New(cfg.SecretsManagement.SecretKey, enc), + } +} diff --git a/pkg/registry/apis/secret/encryption/manager/manager.go b/pkg/registry/apis/secret/encryption/manager/manager.go new file mode 100644 index 00000000000..1c596a3c70f --- /dev/null +++ b/pkg/registry/apis/secret/encryption/manager/manager.go @@ -0,0 +1,403 @@ +package manager + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "strconv" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +const ( + keyIdDelimiter = '#' +) + +type EncryptionManager struct { + tracer trace.Tracer + store contracts.DataKeyStorage + enc cipher.Cipher + cfg *setting.Cfg + usageStats usagestats.Service + + mtx sync.Mutex + + pOnce sync.Once + providers encryption.ProviderMap + + currentProviderID encryption.ProviderID + + log log.Logger +} + +// ProvideEncryptionManager returns an EncryptionManager that uses the OSS KMS providers, along with any additional third-party (e.g. Enterprise) KMS providers +func ProvideEncryptionManager( + tracer trace.Tracer, + store contracts.DataKeyStorage, + cfg *setting.Cfg, + usageStats usagestats.Service, + thirdPartyKMS encryption.ProviderMap, +) (contracts.EncryptionManager, error) { + currentProviderID := encryption.ProviderID(cfg.SecretsManagement.EncryptionProvider) + + enc, err := service.NewEncryptionService(tracer, usageStats, cfg) + if err != nil { + return nil, fmt.Errorf("failed to create encryption service: %w", err) + } + + s := &EncryptionManager{ + tracer: tracer, + store: store, + cfg: cfg, + usageStats: usageStats, + enc: enc, + currentProviderID: currentProviderID, + log: log.New("encryption"), + } + + if err := s.InitProviders(thirdPartyKMS); err != nil { + return nil, err + } + + if _, ok := s.providers[currentProviderID]; !ok { + return nil, fmt.Errorf("missing configuration for current encryption provider %s", currentProviderID) + } + + s.registerUsageMetrics() + + return s, nil +} + +func (s *EncryptionManager) InitProviders(extraProviders encryption.ProviderMap) (err error) { + done := false + s.pOnce.Do(func() { + providers := kmsproviders.GetOSSKMSProviders(s.cfg, s.enc) + + for id, p := range extraProviders { + if _, exists := s.providers[id]; exists { + err = fmt.Errorf("provider %s already registered", id) + return + } + providers[id] = p + } + + s.providers = providers + done = true + }) + + if !done && err == nil { + err = fmt.Errorf("providers were already initialized, no action taken") + } + + return +} + +func (s *EncryptionManager) registerUsageMetrics() { + s.usageStats.RegisterMetricsFunc(func(ctx context.Context) (map[string]any, error) { + usageMetrics := make(map[string]any) + + // Current provider + kind, err := s.currentProviderID.Kind() + if err != nil { + return nil, fmt.Errorf("encryptionManager.registerUsageMetrics: %w", err) + } + usageMetrics[fmt.Sprintf("stats.%s.encryption.current_provider.%s.count", encryption.UsageInsightsPrefix, kind)] = 1 + + // Count by kind + countByKind := make(map[string]int, len(s.providers)) + for id := range s.providers { + kind, err := id.Kind() + if err != nil { + return nil, fmt.Errorf("encryptionManager.registerUsageMetrics: %w", err) + } + + countByKind[kind]++ + } + + for kind, count := range countByKind { + usageMetrics[fmt.Sprintf("stats.%s.encryption.providers.%s.count", encryption.UsageInsightsPrefix, kind)] = count + } + + return usageMetrics, nil + }) +} + +// TODO: Why do we need to use a global variable for this? +var b64 = base64.RawStdEncoding + +func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { + ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Encrypt", trace.WithAttributes( + attribute.String("namespace", namespace), + )) + defer span.End() + + var err error + defer func() { + opsCounter.With(prometheus.Labels{ + "success": strconv.FormatBool(err == nil), + "operation": OpEncrypt, + }).Inc() + + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + } + }() + + label := encryption.KeyLabel(s.currentProviderID) + + var id string + var dataKey []byte + id, dataKey, err = s.currentDataKey(ctx, namespace, label) + if err != nil { + s.log.Error("Failed to get current data key", "error", err, "label", label) + return nil, err + } + + var encrypted []byte + encrypted, err = s.enc.Encrypt(ctx, payload, string(dataKey)) + if err != nil { + s.log.Error("Failed to encrypt secret", "error", err) + return nil, err + } + + prefix := make([]byte, b64.EncodedLen(len(id))+2) + b64.Encode(prefix[1:], []byte(id)) + prefix[0] = keyIdDelimiter + prefix[len(prefix)-1] = keyIdDelimiter + + blob := make([]byte, len(prefix)+len(encrypted)) + copy(blob, prefix) + copy(blob[len(prefix):], encrypted) + + return blob, nil +} + +// currentDataKey looks up for current data key in cache or database by name, and decrypts it. +// If there's no current data key in cache nor in database it generates a new random data key, +// and stores it into both the in-memory cache and database (encrypted by the encryption provider). +func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace string, label string) (string, []byte, error) { + ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.CurrentDataKey", trace.WithAttributes( + attribute.String("namespace", namespace), + attribute.String("label", label), + )) + defer span.End() + + // We want only one request fetching current data key at time to + // avoid the creation of multiple ones in case there's no one existing. + s.mtx.Lock() + defer s.mtx.Unlock() + + // We try to fetch the data key, either from cache or database + id, dataKey, err := s.dataKeyByLabel(ctx, namespace, label) + if err != nil { + return "", nil, err + } + + // If no existing data key was found, create a new one + if dataKey == nil { + id, dataKey, err = s.newDataKey(ctx, namespace, label) + if err != nil { + return "", nil, err + } + } + + return id, dataKey, nil +} + +// dataKeyByLabel looks up for data key in cache by label. +// Otherwise, it fetches it from database, decrypts it and caches it decrypted. +func (s *EncryptionManager) dataKeyByLabel(ctx context.Context, namespace, label string) (string, []byte, error) { + // 1. Get data key from database. + dataKey, err := s.store.GetCurrentDataKey(ctx, namespace, label) + if err != nil { + if errors.Is(err, contracts.ErrDataKeyNotFound) { + return "", nil, nil + } + return "", nil, err + } + + // 2.1 Find the encryption provider. + provider, exists := s.providers[dataKey.Provider] + if !exists { + return "", nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider) + } + + // 2.2 Decrypt the data key fetched from the database. + decrypted, err := provider.Decrypt(ctx, dataKey.EncryptedData) + if err != nil { + return "", nil, err + } + + return dataKey.UID, decrypted, nil +} + +// newDataKey creates a new random data key, encrypts it and stores it into the database. +func (s *EncryptionManager) newDataKey(ctx context.Context, namespace string, label string) (string, []byte, error) { + ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.NewDataKey", trace.WithAttributes( + attribute.String("namespace", namespace), + attribute.String("label", label), + )) + defer span.End() + + // 1. Create new data key. + dataKey, err := newRandomDataKey() + if err != nil { + return "", nil, err + } + + // 2.1 Find the encryption provider. + provider, exists := s.providers[s.currentProviderID] + if !exists { + return "", nil, fmt.Errorf("could not find encryption provider '%s'", s.currentProviderID) + } + + // 2.2 Encrypt the data key. + encrypted, err := provider.Encrypt(ctx, dataKey) + if err != nil { + return "", nil, err + } + + // 3. Store its encrypted value into the DB. + id := util.GenerateShortUID() + + dbDataKey := contracts.SecretDataKey{ + Active: true, + UID: id, + Namespace: namespace, + Provider: s.currentProviderID, + EncryptedData: encrypted, + Label: label, + } + + err = s.store.CreateDataKey(ctx, &dbDataKey) + if err != nil { + return "", nil, err + } + + return id, dataKey, nil +} + +func newRandomDataKey() ([]byte, error) { + rawDataKey := make([]byte, 16) + _, err := rand.Read(rawDataKey) + if err != nil { + return nil, err + } + return rawDataKey, nil +} + +func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { + ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Decrypt", trace.WithAttributes( + attribute.String("namespace", namespace), + )) + defer span.End() + + var err error + defer func() { + opsCounter.With(prometheus.Labels{ + "success": strconv.FormatBool(err == nil), + "operation": OpDecrypt, + }).Inc() + + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + + s.log.FromContext(ctx).Error("Failed to decrypt secret", "error", err) + } + }() + + if len(payload) == 0 { + err = fmt.Errorf("unable to decrypt empty payload") + return nil, err + } + + payload = payload[1:] + endOfKey := bytes.Index(payload, []byte{keyIdDelimiter}) + if endOfKey == -1 { + err = fmt.Errorf("could not find valid key id in encrypted payload") + return nil, err + } + b64Key := payload[:endOfKey] + payload = payload[endOfKey+1:] + keyId := make([]byte, b64.DecodedLen(len(b64Key))) + _, err = b64.Decode(keyId, b64Key) + if err != nil { + return nil, err + } + + dataKey, err := s.dataKeyById(ctx, namespace, string(keyId)) + if err != nil { + s.log.FromContext(ctx).Error("Failed to lookup data key by id", "id", string(keyId), "error", err) + return nil, err + } + + var decrypted []byte + decrypted, err = s.enc.Decrypt(ctx, payload, string(dataKey)) + + return decrypted, err +} + +func (s *EncryptionManager) GetDecryptedValue(ctx context.Context, namespace string, sjd map[string][]byte, key, fallback string) string { + if value, ok := sjd[key]; ok { + decryptedData, err := s.Decrypt(ctx, namespace, value) + if err != nil { + return fallback + } + + return string(decryptedData) + } + + return fallback +} + +// dataKeyById looks up for data key in the database and returns it decrypted. +func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id string) ([]byte, error) { + ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.GetDataKey", trace.WithAttributes( + attribute.String("namespace", namespace), + attribute.String("id", id), + )) + defer span.End() + + // 1. Get encrypted data key from database. + dataKey, err := s.store.GetDataKey(ctx, namespace, id) + if err != nil { + return nil, err + } + + // 2.1. Find the encryption provider. + provider, exists := s.providers[dataKey.Provider] + if !exists { + return nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider) + } + + // 2.2. Decrypt the data key. + decrypted, err := provider.Decrypt(ctx, dataKey.EncryptedData) + if err != nil { + return nil, err + } + + return decrypted, nil +} + +func (s *EncryptionManager) GetProviders() encryption.ProviderMap { + return s.providers +} diff --git a/pkg/registry/apis/secret/encryption/manager/manager_test.go b/pkg/registry/apis/secret/encryption/manager/manager_test.go new file mode 100644 index 00000000000..15cbaa600c5 --- /dev/null +++ b/pkg/registry/apis/secret/encryption/manager/manager_test.go @@ -0,0 +1,448 @@ +package manager + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" + "gopkg.in/ini.v1" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/secret/database" + encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/grafana/grafana/pkg/util" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestEncryptionService_EnvelopeEncryption(t *testing.T) { + svc := setupTestService(t) + ctx := context.Background() + namespace := "test-namespace" + + t.Run("encrypting should create DEK", func(t *testing.T) { + plaintext := []byte("very secret string") + + encrypted, err := svc.Encrypt(context.Background(), namespace, plaintext) + require.NoError(t, err) + + decrypted, err := svc.Decrypt(context.Background(), namespace, encrypted) + require.NoError(t, err) + assert.Equal(t, plaintext, decrypted) + + keys, err := svc.store.GetAllDataKeys(ctx, namespace) + require.NoError(t, err) + assert.Equal(t, len(keys), 1) + }) + + t.Run("encrypting another secret should use the same DEK", func(t *testing.T) { + plaintext := []byte("another very secret string") + + encrypted, err := svc.Encrypt(context.Background(), namespace, plaintext) + require.NoError(t, err) + + decrypted, err := svc.Decrypt(context.Background(), namespace, encrypted) + require.NoError(t, err) + assert.Equal(t, plaintext, decrypted) + + keys, err := svc.store.GetAllDataKeys(ctx, namespace) + require.NoError(t, err) + assert.Equal(t, len(keys), 1) + }) + + t.Run("usage stats should be registered", func(t *testing.T) { + reports, err := svc.usageStats.GetUsageReport(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.current_provider.secretKey.count"]) + assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.providers.secretKey.count"]) + }) +} + +func TestEncryptionService_DataKeys(t *testing.T) { + // Initialize data key storage with a fake db + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) + features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) + tracer := noop.NewTracerProvider().Tracer("test") + store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features) + require.NoError(t, err) + + ctx := context.Background() + namespace := "test-namespace" + + dataKey := &contracts.SecretDataKey{ + UID: util.GenerateShortUID(), + Label: "test1", + Active: true, + Provider: "test", + EncryptedData: []byte{0x62, 0xAF, 0xA1, 0x1A}, + Namespace: namespace, + } + + t.Run("querying for a DEK that does not exist", func(t *testing.T) { + res, err := store.GetDataKey(ctx, namespace, dataKey.UID) + assert.ErrorIs(t, contracts.ErrDataKeyNotFound, err) + assert.Nil(t, res) + }) + + t.Run("creating an active DEK", func(t *testing.T) { + err := store.CreateDataKey(ctx, dataKey) + require.NoError(t, err) + + res, err := store.GetDataKey(ctx, namespace, dataKey.UID) + require.NoError(t, err) + assert.Equal(t, dataKey.EncryptedData, res.EncryptedData) + assert.Equal(t, dataKey.Provider, res.Provider) + assert.Equal(t, dataKey.Label, res.Label) + assert.Equal(t, dataKey.UID, res.UID) + assert.True(t, dataKey.Active) + + current, err := store.GetCurrentDataKey(ctx, namespace, dataKey.Label) + require.NoError(t, err) + assert.Equal(t, dataKey.EncryptedData, current.EncryptedData) + assert.Equal(t, dataKey.Provider, current.Provider) + assert.Equal(t, dataKey.Label, current.Label) + assert.Equal(t, dataKey.UID, current.UID) + assert.True(t, current.Active) + }) + + t.Run("creating an inactive DEK", func(t *testing.T) { + k := &contracts.SecretDataKey{ + UID: util.GenerateShortUID(), + Namespace: namespace, + Active: false, + Label: "test2", + Provider: "test", + EncryptedData: []byte{0x62, 0xAF, 0xA1, 0x1A}, + } + + err := store.CreateDataKey(ctx, k) + require.Error(t, err) + + res, err := store.GetDataKey(ctx, namespace, k.UID) + assert.Equal(t, contracts.ErrDataKeyNotFound, err) + assert.Nil(t, res) + }) + + t.Run("deleting DEK when no id provided must fail", func(t *testing.T) { + beforeDelete, err := store.GetAllDataKeys(ctx, namespace) + require.NoError(t, err) + err = store.DeleteDataKey(ctx, namespace, "") + require.Error(t, err) + + afterDelete, err := store.GetAllDataKeys(ctx, namespace) + require.NoError(t, err) + assert.Equal(t, beforeDelete, afterDelete) + }) + + t.Run("deleting a DEK", func(t *testing.T) { + err := store.DeleteDataKey(ctx, namespace, dataKey.UID) + require.NoError(t, err) + + res, err := store.GetDataKey(ctx, namespace, dataKey.UID) + assert.Equal(t, contracts.ErrDataKeyNotFound, err) + assert.Nil(t, res) + }) +} + +func TestEncryptionService_UseCurrentProvider(t *testing.T) { + t.Run("When encryption_provider is not specified explicitly, should use 'secretKey' as a current provider", func(t *testing.T) { + svc := setupTestService(t) + assert.Equal(t, encryption.ProviderID("secretKey.v1"), svc.currentProviderID) + }) + + t.Run("Should use encrypt/decrypt methods of the current encryption provider", func(t *testing.T) { + rawCfg := ` + [secrets_manager.encryption.fakeProvider.v1] + ` + + raw, err := ini.Load([]byte(rawCfg)) + require.NoError(t, err) + + cfg := &setting.Cfg{ + Raw: raw, + SecretsManagement: setting.SecretsManagerSettings{ + SecretKey: "sdDkslslld", + EncryptionProvider: "secretKey.v1", + }, + } + + features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) + tracer := noop.NewTracerProvider().Tracer("test") + encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features) + require.NoError(t, err) + + encMgr, err := ProvideEncryptionManager( + tracer, + encryptionStore, + cfg, + &usagestats.UsageStatsMock{T: t}, + encryption.ProvideThirdPartyProviderMap(), + ) + require.NoError(t, err) + + encryptionManager := encMgr.(*EncryptionManager) + + //override default provider with fake, and register the fake separately + fake := &fakeProvider{} + encryptionManager.providers[encryption.ProviderID("fakeProvider.v1")] = fake + encryptionManager.currentProviderID = "fakeProvider.v1" + + namespace := "test-namespace" + encrypted, _ := encryptionManager.Encrypt(context.Background(), namespace, []byte{}) + assert.True(t, fake.encryptCalled) + assert.False(t, fake.decryptCalled) + + // encryption manager tries to find a DEK in a cache first before calling provider's decrypt + // to bypass the cache, we set up one more secrets service to test decrypting + svcDecryptMgr, err := ProvideEncryptionManager( + tracer, + encryptionStore, + cfg, + &usagestats.UsageStatsMock{T: t}, + encryption.ProvideThirdPartyProviderMap(), + ) + require.NoError(t, err) + + svcDecrypt := svcDecryptMgr.(*EncryptionManager) + svcDecrypt.providers[encryption.ProviderID("fakeProvider.v1")] = fake + svcDecrypt.currentProviderID = "fakeProvider.v1" + + _, _ = svcDecrypt.Decrypt(context.Background(), namespace, encrypted) + assert.True(t, fake.decryptCalled, "fake provider's decrypt should be called") + }) +} + +type fakeProvider struct { + encryptCalled bool + decryptCalled bool +} + +func (p *fakeProvider) Encrypt(_ context.Context, _ []byte) ([]byte, error) { + p.encryptCalled = true + return []byte{}, nil +} + +func (p *fakeProvider) Decrypt(_ context.Context, _ []byte) ([]byte, error) { + p.decryptCalled = true + return []byte{}, nil +} + +func TestEncryptionService_Decrypt(t *testing.T) { + ctx := context.Background() + namespace := "test-namespace" + + t.Run("empty payload should fail", func(t *testing.T) { + svc := setupTestService(t) + _, err := svc.Decrypt(context.Background(), namespace, []byte("")) + require.Error(t, err) + + assert.Equal(t, "unable to decrypt empty payload", err.Error()) + }) + + t.Run("ee encrypted payload with ee enabled should work", func(t *testing.T) { + svc := setupTestService(t) + ciphertext, err := svc.Encrypt(ctx, namespace, []byte("grafana")) + require.NoError(t, err) + + plaintext, err := svc.Decrypt(ctx, namespace, ciphertext) + assert.NoError(t, err) + assert.Equal(t, []byte("grafana"), plaintext) + }) +} + +func TestIntegration_SecretsService(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + someData := []byte(`some-data`) + namespace := "test-namespace" + + tcs := map[string]func(*testing.T, db.DB, contracts.EncryptionManager){ + "regular": func(t *testing.T, _ db.DB, svc contracts.EncryptionManager) { + // We encrypt some data normally, no transactions implied. + _, err := svc.Encrypt(ctx, namespace, someData) + require.NoError(t, err) + }, + "within successful InTransaction": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) { + require.NoError(t, store.InTransaction(ctx, func(ctx context.Context) error { + // We encrypt some data within a transaction that shares the db session. + _, err := svc.Encrypt(ctx, namespace, someData) + require.NoError(t, err) + + // And the transition succeeds. + return nil + })) + }, + "within unsuccessful InTransaction": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) { + require.NotNil(t, store.InTransaction(ctx, func(ctx context.Context) error { + // We encrypt some data within a transaction that shares the db session. + _, err := svc.Encrypt(ctx, namespace, someData) + require.NoError(t, err) + + // But the transaction fails. + return errors.New("error") + })) + }, + "within unsuccessful InTransaction (plus forced db fetch)": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) { + require.NotNil(t, store.InTransaction(ctx, func(ctx context.Context) error { + // We encrypt some data within a transaction that shares the db session. + encrypted, err := svc.Encrypt(ctx, namespace, someData) + require.NoError(t, err) + + // At this point the data key is not cached yet because + // the transaction haven't been committed yet, + // and won't, so we do a decrypt operation within the + // transaction to force the data key to be + // (potentially) cached (it shouldn't to prevent issues). + decrypted, err := svc.Decrypt(ctx, namespace, encrypted) + require.NoError(t, err) + assert.Equal(t, someData, decrypted) + + // But the transaction fails. + return errors.New("error") + })) + }, + "within successful WithTransactionalDbSession": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) { + require.NoError(t, store.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + // We encrypt some data within a transaction that does not share the db session. + _, err := svc.Encrypt(ctx, namespace, someData) + require.NoError(t, err) + + // And the transition succeeds. + return nil + })) + }, + "within unsuccessful WithTransactionalDbSession": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) { + require.NotNil(t, store.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + // We encrypt some data within a transaction that does not share the db session. + _, err := svc.Encrypt(ctx, namespace, someData) + require.NoError(t, err) + + // But the transaction fails. + return errors.New("error") + })) + }, + "within unsuccessful WithTransactionalDbSession (plus forced db fetch)": func(t *testing.T, store db.DB, svc contracts.EncryptionManager) { + require.NotNil(t, store.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + // We encrypt some data within a transaction that does not share the db session. + encrypted, err := svc.Encrypt(ctx, namespace, someData) + require.NoError(t, err) + + // At this point the data key is not cached yet because + // the transaction haven't been committed yet, + // and won't, so we do a decrypt operation within the + // transaction to force the data key to be + // (potentially) cached (it shouldn't to prevent issues). + decrypted, err := svc.Decrypt(ctx, namespace, encrypted) + require.NoError(t, err) + assert.Equal(t, someData, decrypted) + + // But the transaction fails. + return errors.New("error") + })) + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) + tracer := noop.NewTracerProvider().Tracer("test") + + features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) + defaultKey := "SdlklWklckeLS" + + cfg := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + SecretKey: defaultKey, + EncryptionProvider: "secretKey.v1", + }, + } + store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features) + require.NoError(t, err) + + usageStats := &usagestats.UsageStatsMock{T: t} + + svc, err := ProvideEncryptionManager( + tracer, + store, + cfg, + usageStats, + encryption.ProvideThirdPartyProviderMap(), + ) + require.NoError(t, err) + + ctx := context.Background() + namespace := "test-namespace" + + // Here's what actually matters and varies on each test: look at the test case name. + // + // For historical reasons, and in an old implementation, when a successful encryption + // operation happened within an unsuccessful transaction, the data key was used to be + // cached in memory for the next encryption operations, which caused some data to be + // encrypted with a data key that haven't actually been persisted into the database. + tc(t, testDB, svc) + // Therefore, the data encrypted after this point, become unrecoverable after a restart. + // So, the different test cases here are there to prevent that from happening again + // in the future, whatever it is what happens. + + // So, we proceed with an encryption operation: + toEncrypt := []byte(`data-to-encrypt`) + encrypted, err := svc.Encrypt(ctx, namespace, toEncrypt) + require.NoError(t, err) + + // And then, we MUST still be able to decrypt the previously encrypted data: + decrypted, err := svc.Decrypt(ctx, namespace, encrypted) + require.NoError(t, err) + assert.Equal(t, toEncrypt, decrypted) + }) + } +} + +func TestEncryptionService_ReInitReturnsError(t *testing.T) { + svc := setupTestService(t) + err := svc.InitProviders(encryption.ProviderMap{ + "fakeProvider.v1": &fakeProvider{}, + }) + require.Error(t, err) +} + +func TestEncryptionService_ThirdPartyProviders(t *testing.T) { + cfg := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + SecretKey: "SdlklWklckeLS", + EncryptionProvider: "secretKey.v1", + }, + } + + svc, err := ProvideEncryptionManager( + nil, + nil, + cfg, + &usagestats.UsageStatsMock{}, + encryption.ProviderMap{ + "fakeProvider.v1": &fakeProvider{}, + }, + ) + require.NoError(t, err) + + encMgr := svc.(*EncryptionManager) + require.Len(t, encMgr.providers, 2) + require.Contains(t, encMgr.providers, encryption.ProviderID("fakeProvider.v1")) +} diff --git a/pkg/registry/apis/secret/encryption/manager/metrics.go b/pkg/registry/apis/secret/encryption/manager/metrics.go new file mode 100644 index 00000000000..5ff49c7e509 --- /dev/null +++ b/pkg/registry/apis/secret/encryption/manager/metrics.go @@ -0,0 +1,51 @@ +package manager + +import ( + "github.com/prometheus/client_golang/prometheus" + + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" +) + +const ( + OpEncrypt = "encrypt" + OpDecrypt = "decrypt" + subsystem = "encryption_manager" +) + +// TODO: Add timing metrics after the encryption module cleanup +var ( + opsCounter = metricutil.NewCounterVecStartingAtZero( + prometheus.CounterOpts{ + Namespace: metrics.ExporterName, + Subsystem: subsystem, + Name: "encryption_ops_total", + Help: "A counter for encryption operations", + }, + []string{"success", "operation"}, + map[string][]string{ + "success": {"true", "false"}, + "operation": {OpEncrypt, OpDecrypt}, + }, + ) + cacheReadsCounter = metricutil.NewCounterVecStartingAtZero( + prometheus.CounterOpts{ + Namespace: metrics.ExporterName, + Subsystem: subsystem, + Name: "encryption_cache_reads_total", + Help: "A counter for encryption cache reads", + }, + []string{"hit", "method"}, + map[string][]string{ + "hit": {"true", "false"}, + "method": {"byId", "byName"}, + }, + ) +) + +func init() { + prometheus.MustRegister( + opsCounter, + cacheReadsCounter, + ) +} diff --git a/pkg/registry/apis/secret/encryption/manager/test_helpers.go b/pkg/registry/apis/secret/encryption/manager/test_helpers.go new file mode 100644 index 00000000000..bfdd4cc7bea --- /dev/null +++ b/pkg/registry/apis/secret/encryption/manager/test_helpers.go @@ -0,0 +1,49 @@ +package manager + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" + + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + "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" +) + +func setupTestService(tb testing.TB) *EncryptionManager { + tb.Helper() + + testDB := sqlstore.NewTestStore(tb, sqlstore.WithMigrator(migrator.New())) + tracer := noop.NewTracerProvider().Tracer("test") + database := database.ProvideDatabase(testDB, tracer) + + features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) + defaultKey := "SdlklWklckeLS" + cfg := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + SecretKey: defaultKey, + EncryptionProvider: "secretKey.v1", + }, + } + store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features) + require.NoError(tb, err) + + usageStats := &usagestats.UsageStatsMock{T: tb} + + encMgr, err := ProvideEncryptionManager( + tracer, + store, + cfg, + usageStats, + encryption.ProvideThirdPartyProviderMap(), + ) + require.NoError(tb, err) + + return encMgr.(*EncryptionManager) +} diff --git a/pkg/registry/apis/secret/encryption/secrets.go b/pkg/registry/apis/secret/encryption/secrets.go index d1cfd87286a..55a6d36c6cd 100644 --- a/pkg/registry/apis/secret/encryption/secrets.go +++ b/pkg/registry/apis/secret/encryption/secrets.go @@ -1,6 +1,7 @@ package encryption import ( + "context" "fmt" "strings" "time" @@ -8,6 +9,12 @@ import ( const UsageInsightsPrefix = "secrets_manager" +// Provider is a key encryption key provider for envelope encryption +type Provider interface { + Encrypt(ctx context.Context, blob []byte) ([]byte, error) + Decrypt(ctx context.Context, blob []byte) ([]byte, error) +} + type ProviderID string func (id ProviderID) Kind() (string, error) { @@ -24,3 +31,15 @@ func (id ProviderID) Kind() (string, error) { func KeyLabel(providerID ProviderID) string { return fmt.Sprintf("%s@%s", time.Now().Format("2006-01-02"), providerID) } + +type ProviderMap map[ProviderID]Provider + +// ProvideThirdPartyProviderMap fulfills the wire dependency needed by the encryption manager in OSS +func ProvideThirdPartyProviderMap() ProviderMap { + return ProviderMap{} +} + +// BackgroundProvider should be implemented for a provider that has a task that needs to be run in the background. +type BackgroundProvider interface { + Run(ctx context.Context) error +} diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go index e48f8b95a93..d2c5aea98d4 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go @@ -7,8 +7,15 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" + "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" ) @@ -16,8 +23,13 @@ func TestMain(m *testing.M) { testsuite.Run(m) } -func Test_OSSKeeperService_GetKeepers(t *testing.T) { - cfg := setting.NewCfg() +func Test_OSSKeeperService(t *testing.T) { + cfg := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + SecretKey: "sdDkslslld", + EncryptionProvider: "secretKey.v1", + }, + } keeperService, err := setupTestService(t, cfg) require.NoError(t, err) @@ -31,10 +43,23 @@ func Test_OSSKeeperService_GetKeepers(t *testing.T) { } func setupTestService(t *testing.T, cfg *setting.Cfg) (*OSSKeeperService, error) { + // Initialize data key storage and encrypted value storage with a fake db + 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) + + dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features) + require.NoError(t, err) + + encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features) + require.NoError(t, err) + + encryptionManager, err := manager.ProvideEncryptionManager(tracer, dataKeyStore, cfg, &usagestats.UsageStatsMock{T: t}, nil) + require.NoError(t, err) // Initialize the keeper service - keeperService, err := ProvideService(tracer, nil, nil) + keeperService, err := ProvideService(tracer, encValueStore, encryptionManager) return keeperService, err } diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go index 585b124941f..500d04bdf0e 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go @@ -2,20 +2,29 @@ package sqlkeeper import ( "context" - "encoding/base64" - "fmt" - "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + encryptionmanager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/secret/database" + encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/tests/testsuite" ) -// Make this a `TestIntegration` once we have the real storage implementation +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func Test_SQLKeeperSetup(t *testing.T) { ctx := context.Background() namespace1 := "namespace1" @@ -24,50 +33,57 @@ func Test_SQLKeeperSetup(t *testing.T) { plaintext2 := "very secret string in namespace 2" nonExistentID := contracts.ExternalID("non existent") - cfg := setting.NewCfg() + cfg := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + SecretKey: "sdDkslslld", + EncryptionProvider: "secretKey.v1", + }, + } sqlKeeper, err := setupTestService(t, cfg) require.NoError(t, err) require.NotNil(t, sqlKeeper) + keeperCfg := &secretv0alpha1.SystemKeeperConfig{} + t.Run("storing an encrypted value returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalId1) - externalId2, err := sqlKeeper.Store(ctx, nil, namespace2, plaintext2) + externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext2) require.NoError(t, err) require.NotEmpty(t, externalId2) t.Run("expose the encrypted value from existing namespace", func(t *testing.T) { - exposedVal1, err := sqlKeeper.Expose(ctx, nil, namespace1, externalId1) + exposedVal1, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId1) require.NoError(t, err) require.NotNil(t, exposedVal1) assert.Equal(t, plaintext1, exposedVal1.DangerouslyExposeAndConsumeValue()) - exposedVal2, err := sqlKeeper.Expose(ctx, nil, namespace2, externalId2) + exposedVal2, err := sqlKeeper.Expose(ctx, keeperCfg, namespace2, externalId2) require.NoError(t, err) require.NotNil(t, exposedVal2) assert.Equal(t, plaintext2, exposedVal2.DangerouslyExposeAndConsumeValue()) }) t.Run("expose encrypted value from different namespace returns error", func(t *testing.T) { - exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace2, externalId1) + exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace2, externalId1) require.Error(t, err) assert.Empty(t, exposedVal) - exposedVal, err = sqlKeeper.Expose(ctx, nil, namespace1, externalId2) + exposedVal, err = sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId2) require.Error(t, err) assert.Empty(t, exposedVal) }) }) t.Run("storing same value in same namespace returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalId1) - externalId2, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalId2) @@ -75,11 +91,11 @@ func Test_SQLKeeperSetup(t *testing.T) { }) t.Run("storing same value in different namespace returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalId1) - externalId2, err := sqlKeeper.Store(ctx, nil, namespace2, plaintext1) + externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalId2) @@ -87,46 +103,46 @@ func Test_SQLKeeperSetup(t *testing.T) { }) t.Run("exposing non existing values returns error", func(t *testing.T) { - exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, nonExistentID) + exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, nonExistentID) require.Error(t, err) assert.Empty(t, exposedVal) }) t.Run("deleting an existing encrypted value does not return error", func(t *testing.T) { - externalID, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + externalID, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalID) - exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, externalID) + exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalID) require.NoError(t, err) assert.NotNil(t, exposedVal) assert.Equal(t, plaintext1, exposedVal.DangerouslyExposeAndConsumeValue()) - err = sqlKeeper.Delete(ctx, nil, namespace1, externalID) + err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, externalID) require.NoError(t, err) }) t.Run("deleting an non existing encrypted value does not return error", func(t *testing.T) { - err = sqlKeeper.Delete(ctx, nil, namespace1, nonExistentID) + err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, nonExistentID) require.NoError(t, err) }) t.Run("updating an existent encrypted value returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalId1) - err = sqlKeeper.Update(ctx, nil, namespace1, externalId1, plaintext2) + err = sqlKeeper.Update(ctx, keeperCfg, namespace1, externalId1, plaintext2) require.NoError(t, err) - exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, externalId1) + exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId1) require.NoError(t, err) assert.NotNil(t, exposedVal) assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue()) }) t.Run("updating a non existent encrypted value returns error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) require.NoError(t, err) require.NotEmpty(t, externalId1) @@ -136,104 +152,33 @@ func Test_SQLKeeperSetup(t *testing.T) { } func setupTestService(t *testing.T, cfg *setting.Cfg) (*SQLKeeper, error) { + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) tracer := noop.NewTracerProvider().Tracer("test") + database := database.ProvideDatabase(testDB, tracer) - // Initialize the encryption manager with in-memory implementation - encMgr := &inMemoryEncryptionManager{} + features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) - // Initialize encrypted value storage with in-memory implementation - encValueStore := newInMemoryEncryptedValueStorage() + // Initialize the encryption manager + dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features) + require.NoError(t, err) + + usageStats := &usagestats.UsageStatsMock{T: t} + + encMgr, err := encryptionmanager.ProvideEncryptionManager( + tracer, + dataKeyStore, + cfg, + usageStats, + nil, + ) + require.NoError(t, err) + + // Initialize encrypted value storage with a fake db + encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features) + require.NoError(t, err) // Initialize the SQLKeeper sqlKeeper := NewSQLKeeper(tracer, encMgr, encValueStore) return sqlKeeper, nil } - -// While we don't have the real implementation, use an in-memory one -type inMemoryEncryptionManager struct{} - -func (m *inMemoryEncryptionManager) Encrypt(_ context.Context, _ string, value []byte) ([]byte, error) { - return []byte(base64.StdEncoding.EncodeToString(value)), nil -} - -func (m *inMemoryEncryptionManager) Decrypt(_ context.Context, _ string, value []byte) ([]byte, error) { - return base64.StdEncoding.DecodeString(string(value)) -} - -func (m *inMemoryEncryptionManager) ReEncryptDataKeys(_ context.Context, _ string) error { - return nil -} - -func (m *inMemoryEncryptionManager) RotateDataKeys(_ context.Context, _ string) error { - return nil -} - -// While we don't have the real implementation, use an in-memory one -type inMemoryEncryptedValueStorage struct { - mu sync.RWMutex - store map[string]*contracts.EncryptedValue -} - -func newInMemoryEncryptedValueStorage() *inMemoryEncryptedValueStorage { - return &inMemoryEncryptedValueStorage{ - store: make(map[string]*contracts.EncryptedValue), - } -} - -func (m *inMemoryEncryptedValueStorage) Create(_ context.Context, namespace string, encryptedData []byte) (*contracts.EncryptedValue, error) { - m.mu.Lock() - defer m.mu.Unlock() - - uid := fmt.Sprintf("%d", len(m.store)+1) // Generate simple incremental IDs - encValue := &contracts.EncryptedValue{ - UID: uid, - Namespace: namespace, - EncryptedData: encryptedData, - Created: 1, // Dummy timestamp - Updated: 1, // Dummy timestamp - } - - compositeKey := namespace + ":" + uid - m.store[compositeKey] = encValue - - return encValue, nil -} - -func (m *inMemoryEncryptedValueStorage) Get(_ context.Context, namespace string, uid string) (*contracts.EncryptedValue, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - compositeKey := namespace + ":" + uid - encValue, exists := m.store[compositeKey] - if !exists { - return nil, fmt.Errorf("value not found for namespace %s and uid %s", namespace, uid) - } - - return encValue, nil -} - -func (m *inMemoryEncryptedValueStorage) Delete(_ context.Context, namespace string, uid string) error { - m.mu.Lock() - defer m.mu.Unlock() - - compositeKey := namespace + ":" + uid - delete(m.store, compositeKey) - - return nil -} - -func (m *inMemoryEncryptedValueStorage) Update(_ context.Context, namespace string, uid string, encryptedData []byte) error { - m.mu.Lock() - defer m.mu.Unlock() - - compositeKey := namespace + ":" + uid - encValue, exists := m.store[compositeKey] - if !exists { - return fmt.Errorf("value not found for namespace %s and uid %s", namespace, uid) - } - - encValue.EncryptedData = encryptedData - encValue.Updated = 2 // Update timestamp - return nil -} diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 2b8f7352f7e..406497bcf9f 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -43,6 +43,8 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" secretcontracts "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" secretdecrypt "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" + gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" appregistry "github.com/grafana/grafana/pkg/registry/apps" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -428,6 +430,8 @@ var wireBasicSet = wire.NewSet( secretmigrator.NewWithEngine, secretdatabase.ProvideDatabase, wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)), + encryptionManager.ProvideEncryptionManager, + gsmEncryption.ProvideThirdPartyProviderMap, secretdecrypt.ProvideDecryptAuthorizer, secretdecrypt.ProvideDecryptAllowList, // Unified storage diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 5657b0e4c14..9fba20aa8c6 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -61,6 +61,8 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" + encryption3 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + manager4 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/userstorage" "github.com/grafana/grafana/pkg/registry/apps" advisor2 "github.com/grafana/grafana/pkg/registry/apps/advisor" @@ -1426,7 +1428,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideOutboxQueue, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideOutboxQueue, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption3.ProvideThirdPartyProviderMap, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 28f116c2f9c..4c2e5f8b14d 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -11,6 +11,8 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper" "github.com/grafana/grafana/pkg/registry/backgroundsvcs" "github.com/grafana/grafana/pkg/registry/usagestatssvcs" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -93,6 +95,8 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)), osskmsproviders.ProvideService, wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), + secretkeeper.ProvideService, + wire.Bind(new(contracts.KeeperService), new(*secretkeeper.OSSKeeperService)), ldap.ProvideGroupsService, wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)), guardian.ProvideGuardian, diff --git a/pkg/setting/setting_secrets_manager.go b/pkg/setting/setting_secrets_manager.go index 646acd77546..f52021a9ebf 100644 --- a/pkg/setting/setting_secrets_manager.go +++ b/pkg/setting/setting_secrets_manager.go @@ -4,7 +4,6 @@ import ( "regexp" "time" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" "github.com/grafana/grafana/pkg/services/kmsproviders" ) @@ -18,8 +17,6 @@ type SecretsManagerSettings struct { SecretKey string EncryptionProvider string AvailableProviders []string - - Encryption EncryptionSettings } func (cfg *Cfg) readSecretsManagerSettings() { @@ -29,9 +26,4 @@ func (cfg *Cfg) readSecretsManagerSettings() { // TODO: These are not used yet by the secrets manager because we need to distentagle the dependencies with OSS. cfg.SecretsManagement.SecretKey = secretsMgmt.Key("secret_key").MustString("") cfg.SecretsManagement.AvailableProviders = regexp.MustCompile(`\s*,\s*`).Split(secretsMgmt.Key("available_encryption_providers").MustString(""), -1) // parse comma separated list - - encryption := cfg.Raw.Section("secrets_manager.encryption") - cfg.SecretsManagement.Encryption.DataKeysCacheTTL = encryption.Key("data_keys_cache_ttl").MustDuration(15 * time.Minute) - cfg.SecretsManagement.Encryption.DataKeysCleanupInterval = encryption.Key("data_keys_cache_cleanup_interval").MustDuration(1 * time.Minute) - cfg.SecretsManagement.Encryption.Algorithm = encryption.Key("algorithm").MustString(cipher.AesGcm) } diff --git a/pkg/storage/secret/encryption/encrypted_value_store.go b/pkg/storage/secret/encryption/encrypted_value_store.go index 58bdd4054a2..a369a7a74f5 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store.go +++ b/pkg/storage/secret/encryption/encrypted_value_store.go @@ -19,7 +19,11 @@ var ( ErrEncryptedValueNotFound = errors.New("encrypted value not found") ) -func ProvideEncryptedValueStorage(db contracts.Database, tracer trace.Tracer, features featuremgmt.FeatureToggles) (contracts.EncryptedValueStorage, error) { +func ProvideEncryptedValueStorage( + db contracts.Database, + tracer trace.Tracer, + features featuremgmt.FeatureToggles, +) (contracts.EncryptedValueStorage, error) { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { return &encryptedValStorage{}, nil