Alerting: idempotent decrypt\encrypt operations on extra configs (#107592)
* add prefix to encrypted string to distinguish between unencrypted and encrypted ones --------- Co-authored-by: Alexander Akhmetov <me@alx.cx>
This commit is contained in:
co-authored by
Alexander Akhmetov
parent
d15e1ad8d0
commit
c93fd3ee9e
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
@@ -14,6 +15,16 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// encryptedContentPrefix is a marker that identifies encrypted Alertmanager configurations.
|
||||||
|
// When this prefix is present at the beginning of a configuration string:
|
||||||
|
// 1. During encryption: It indicates the content is already encrypted and should be skipped
|
||||||
|
// 2. During decryption: It indicates the content (minus this prefix) should be base64 decoded
|
||||||
|
// and then decrypted using the secrets service
|
||||||
|
// This prefix helps maintain idempotency in encryption/decryption operations.
|
||||||
|
cryptoPrefix = "crypto_"
|
||||||
|
)
|
||||||
|
|
||||||
// Crypto allows decryption of Alertmanager Configuration and encryption of arbitrary payloads.
|
// Crypto allows decryption of Alertmanager Configuration and encryption of arbitrary payloads.
|
||||||
type Crypto interface {
|
type Crypto interface {
|
||||||
LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error
|
LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error
|
||||||
@@ -253,12 +264,17 @@ func NewExtraConfigsCrypto(secrets secretService) *ExtraConfigsCrypto {
|
|||||||
|
|
||||||
func (c *ExtraConfigsCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
|
func (c *ExtraConfigsCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
|
||||||
for i := range config.ExtraConfigs {
|
for i := range config.ExtraConfigs {
|
||||||
|
// If it has prefix, consider it encrypted already
|
||||||
|
if strings.HasPrefix(config.ExtraConfigs[i].AlertmanagerConfig, cryptoPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
encryptedValue, err := c.secrets.Encrypt(ctx, []byte(config.ExtraConfigs[i].AlertmanagerConfig), secrets.WithoutScope())
|
encryptedValue, err := c.secrets.Encrypt(ctx, []byte(config.ExtraConfigs[i].AlertmanagerConfig), secrets.WithoutScope())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to encrypt extra configuration: %w", err)
|
return fmt.Errorf("failed to encrypt extra configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.ExtraConfigs[i].AlertmanagerConfig = base64.StdEncoding.EncodeToString(encryptedValue)
|
config.ExtraConfigs[i].AlertmanagerConfig = cryptoPrefix + base64.StdEncoding.EncodeToString(encryptedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -266,12 +282,15 @@ func (c *ExtraConfigsCrypto) EncryptExtraConfigs(ctx context.Context, config *de
|
|||||||
|
|
||||||
func (c *ExtraConfigsCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
|
func (c *ExtraConfigsCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
|
||||||
for i := range config.ExtraConfigs {
|
for i := range config.ExtraConfigs {
|
||||||
// Check if the config is encrypted by trying to base64 decode it
|
// If it does not have prefix, consider it decrypted already
|
||||||
encryptedValue, err := base64.StdEncoding.DecodeString(config.ExtraConfigs[i].AlertmanagerConfig)
|
if !strings.HasPrefix(config.ExtraConfigs[i].AlertmanagerConfig, cryptoPrefix) {
|
||||||
if err != nil {
|
|
||||||
// If it can't be base64 decoded, assume it's already decrypted and skip
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Check if the config is encrypted by trying to base64 decode it
|
||||||
|
encryptedValue, err := base64.StdEncoding.DecodeString(config.ExtraConfigs[i].AlertmanagerConfig[len(cryptoPrefix):])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode extra configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
decryptedValue, err := c.secrets.Decrypt(ctx, encryptedValue)
|
decryptedValue, err := c.secrets.Decrypt(ctx, encryptedValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
|
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncryptExtraConfigs(t *testing.T) {
|
||||||
|
config := "plain-text-config"
|
||||||
|
encryptedConfig := base64.StdEncoding.EncodeToString([]byte(config))
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputConfig string
|
||||||
|
expectedConfig string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Encrypts unencrypted configs",
|
||||||
|
inputConfig: config,
|
||||||
|
expectedConfig: cryptoPrefix + encryptedConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Skips already encrypted configs",
|
||||||
|
inputConfig: cryptoPrefix + "very-encrypted-data",
|
||||||
|
expectedConfig: cryptoPrefix + "very-encrypted-data",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m := fakes.NewFakeSecretsService()
|
||||||
|
|
||||||
|
c := &alertmanagerCrypto{
|
||||||
|
secrets: m,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &definitions.PostableUserConfig{
|
||||||
|
ExtraConfigs: []definitions.ExtraConfiguration{
|
||||||
|
{AlertmanagerConfig: tt.inputConfig},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.EncryptExtraConfigs(context.Background(), cfg)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expectedConfig, cfg.ExtraConfigs[0].AlertmanagerConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptExtraConfigs(t *testing.T) {
|
||||||
|
decryptedData := "derypted-data"
|
||||||
|
decryptedDataBase64 := base64.StdEncoding.EncodeToString([]byte(decryptedData))
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputConfig string
|
||||||
|
expectedError string
|
||||||
|
expectedConfig string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Decrypts encrypted configs",
|
||||||
|
inputConfig: cryptoPrefix + decryptedDataBase64,
|
||||||
|
expectedConfig: decryptedData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Skips already encrypted configs",
|
||||||
|
inputConfig: "very-decrypted-data",
|
||||||
|
expectedConfig: "very-decrypted-data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fails if not base64 encoded",
|
||||||
|
inputConfig: cryptoPrefix + "plain-text-config",
|
||||||
|
expectedError: "failed to decode extra configuration",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m := fakes.NewFakeSecretsService()
|
||||||
|
c := &alertmanagerCrypto{
|
||||||
|
secrets: m,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &definitions.PostableUserConfig{
|
||||||
|
ExtraConfigs: []definitions.ExtraConfiguration{
|
||||||
|
{AlertmanagerConfig: tt.inputConfig},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.DecryptExtraConfigs(context.Background(), cfg)
|
||||||
|
|
||||||
|
if tt.expectedError != "" {
|
||||||
|
require.ErrorContains(t, err, tt.expectedError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expectedConfig, cfg.ExtraConfigs[0].AlertmanagerConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user