Alerting: Use new receiver models for encrypt/decrypt in remote AM (#107042)
Several niche bugs have surfaced as a result of the decrypt code Grafana uses in receivers API being different than what is used to decrypt secrets before sending to remote AM. Example: - Dingding notifier not abiding by new Patching added to local AM, thus causing missing url errors. * noop refactor to simplify decryptConfiguration * Move compat function package * Use new receiver models to encrypt/decrypt in remote AM
This commit is contained in:
@@ -2,102 +2,13 @@ package notifier
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
)
|
||||
|
||||
func PostableApiReceiversToReceivers(postables []*apimodels.PostableApiReceiver, storedProvenances map[string]models.Provenance) ([]*models.Receiver, error) {
|
||||
receivers := make([]*models.Receiver, 0, len(postables))
|
||||
for _, postable := range postables {
|
||||
r, err := PostableApiReceiverToReceiver(postable, getReceiverProvenance(storedProvenances, postable))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivers = append(receivers, r)
|
||||
}
|
||||
return receivers, nil
|
||||
}
|
||||
|
||||
func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, provenance models.Provenance) (*models.Receiver, error) {
|
||||
integrations, err := PostableGrafanaReceiversToIntegrations(postable.GrafanaManagedReceivers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := &models.Receiver{
|
||||
UID: legacy_storage.NameToUid(postable.GetName()), // TODO replace with stable UID.
|
||||
Name: postable.GetName(),
|
||||
Integrations: integrations,
|
||||
Provenance: provenance,
|
||||
}
|
||||
r.Version = r.Fingerprint()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func PostableGrafanaReceiversToIntegrations(postables []*apimodels.PostableGrafanaReceiver) ([]*models.Integration, error) {
|
||||
integrations := make([]*models.Integration, 0, len(postables))
|
||||
for _, cfg := range postables {
|
||||
integration, err := PostableGrafanaReceiverToIntegration(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
integrations = append(integrations, integration)
|
||||
}
|
||||
|
||||
return integrations, nil
|
||||
}
|
||||
|
||||
func PostableGrafanaReceiverToIntegration(p *apimodels.PostableGrafanaReceiver) (*models.Integration, error) {
|
||||
config, err := models.IntegrationConfigFromType(p.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
integration := &models.Integration{
|
||||
UID: p.UID,
|
||||
Name: p.Name,
|
||||
Config: config,
|
||||
DisableResolveMessage: p.DisableResolveMessage,
|
||||
Settings: make(map[string]any, len(p.Settings)),
|
||||
SecureSettings: make(map[string]string, len(p.SecureSettings)),
|
||||
}
|
||||
|
||||
if p.Settings != nil {
|
||||
if err := json.Unmarshal(p.Settings, &integration.Settings); err != nil {
|
||||
return nil, fmt.Errorf("integration '%s' of receiver '%s' has settings that cannot be parsed as JSON: %w", integration.Config.Type, p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range p.SecureSettings {
|
||||
if v != "" {
|
||||
integration.SecureSettings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return integration, nil
|
||||
}
|
||||
|
||||
// getReceiverProvenance determines the provenance of a definitions.PostableApiReceiver based on the provenance of its integrations.
|
||||
func getReceiverProvenance(storedProvenances map[string]models.Provenance, r *apimodels.PostableApiReceiver) models.Provenance {
|
||||
if len(r.GrafanaManagedReceivers) == 0 {
|
||||
return models.ProvenanceNone
|
||||
}
|
||||
|
||||
// Current provisioning works on the integration level, so we need some way to determine the provenance of the
|
||||
// entire receiver. All integrations in a receiver should have the same provenance, but we don't want to rely on
|
||||
// this assumption in case the first provenance is None and a later one is not. To this end, we return the first
|
||||
// non-zero provenance we find.
|
||||
for _, contactPoint := range r.GrafanaManagedReceivers {
|
||||
if p, exists := storedProvenances[contactPoint.UID]; exists && p != models.ProvenanceNone {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return models.ProvenanceNone
|
||||
}
|
||||
|
||||
func PostableGrafanaReceiverToGrafanaIntegrationConfig(p *apimodels.PostableGrafanaReceiver) *alertingNotify.GrafanaIntegrationConfig {
|
||||
return &alertingNotify.GrafanaIntegrationConfig{
|
||||
UID: p.UID,
|
||||
|
||||
@@ -3,6 +3,7 @@ package legacy_storage
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
@@ -63,3 +64,90 @@ func ReceiverToPostableApiReceiver(r *models.Receiver) (*apimodels.PostableApiRe
|
||||
PostableGrafanaReceivers: integrations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PostableApiReceiversToReceivers(postables []*apimodels.PostableApiReceiver, storedProvenances map[string]models.Provenance) ([]*models.Receiver, error) {
|
||||
receivers := make([]*models.Receiver, 0, len(postables))
|
||||
for _, postable := range postables {
|
||||
r, err := PostableApiReceiverToReceiver(postable, GetReceiverProvenance(storedProvenances, postable))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivers = append(receivers, r)
|
||||
}
|
||||
return receivers, nil
|
||||
}
|
||||
|
||||
func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, provenance models.Provenance) (*models.Receiver, error) {
|
||||
integrations, err := PostableGrafanaReceiversToIntegrations(postable.GrafanaManagedReceivers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := &models.Receiver{
|
||||
UID: NameToUid(postable.GetName()), // TODO replace with stable UID.
|
||||
Name: postable.GetName(),
|
||||
Integrations: integrations,
|
||||
Provenance: provenance,
|
||||
}
|
||||
r.Version = r.Fingerprint()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// GetReceiverProvenance determines the provenance of a definitions.PostableApiReceiver based on the provenance of its integrations.
|
||||
func GetReceiverProvenance(storedProvenances map[string]models.Provenance, r *apimodels.PostableApiReceiver) models.Provenance {
|
||||
if len(r.GrafanaManagedReceivers) == 0 {
|
||||
return models.ProvenanceNone
|
||||
}
|
||||
|
||||
// Current provisioning works on the integration level, so we need some way to determine the provenance of the
|
||||
// entire receiver. All integrations in a receiver should have the same provenance, but we don't want to rely on
|
||||
// this assumption in case the first provenance is None and a later one is not. To this end, we return the first
|
||||
// non-zero provenance we find.
|
||||
for _, contactPoint := range r.GrafanaManagedReceivers {
|
||||
if p, exists := storedProvenances[contactPoint.UID]; exists && p != models.ProvenanceNone {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return models.ProvenanceNone
|
||||
}
|
||||
|
||||
func PostableGrafanaReceiversToIntegrations(postables []*apimodels.PostableGrafanaReceiver) ([]*models.Integration, error) {
|
||||
integrations := make([]*models.Integration, 0, len(postables))
|
||||
for _, cfg := range postables {
|
||||
integration, err := PostableGrafanaReceiverToIntegration(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
integrations = append(integrations, integration)
|
||||
}
|
||||
|
||||
return integrations, nil
|
||||
}
|
||||
|
||||
func PostableGrafanaReceiverToIntegration(p *apimodels.PostableGrafanaReceiver) (*models.Integration, error) {
|
||||
config, err := models.IntegrationConfigFromType(p.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
integration := &models.Integration{
|
||||
UID: p.UID,
|
||||
Name: p.Name,
|
||||
Config: config,
|
||||
DisableResolveMessage: p.DisableResolveMessage,
|
||||
Settings: make(map[string]any, len(p.Settings)),
|
||||
SecureSettings: make(map[string]string, len(p.SecureSettings)),
|
||||
}
|
||||
|
||||
if p.Settings != nil {
|
||||
if err := json.Unmarshal(p.Settings, &integration.Settings); err != nil {
|
||||
return nil, fmt.Errorf("integration '%s' of receiver '%s' has settings that cannot be parsed as JSON: %w", integration.Config.Type, p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range p.SecureSettings {
|
||||
if v != "" {
|
||||
integration.SecureSettings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return integration, nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,52 @@ func (rev *ConfigRevision) GetReceivers(uids []string) []*definitions.PostableAp
|
||||
return receivers
|
||||
}
|
||||
|
||||
func DecryptedReceivers(receivers []*definitions.PostableApiReceiver, decryptFn models.DecryptFn) ([]*definitions.PostableApiReceiver, error) {
|
||||
decrypted := make([]*definitions.PostableApiReceiver, len(receivers))
|
||||
for i, r := range receivers {
|
||||
// We don't care about the provenance here, so we pass ProvenanceNone.
|
||||
rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = rcv.Decrypt(decryptFn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt receiver %q: %w", rcv.Name, err)
|
||||
}
|
||||
|
||||
postable, err := ReceiverToPostableApiReceiver(rcv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert Receiver %q to APIReceiver: %w", rcv.Name, err)
|
||||
}
|
||||
decrypted[i] = postable
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func EncryptedReceivers(receivers []*definitions.PostableApiReceiver, encryptFn models.EncryptFn) ([]*definitions.PostableApiReceiver, error) {
|
||||
encrypted := make([]*definitions.PostableApiReceiver, len(receivers))
|
||||
for i, r := range receivers {
|
||||
// We don't care about the provenance here, so we pass ProvenanceNone.
|
||||
rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = rcv.Encrypt(encryptFn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt receiver %q: %w", rcv.Name, err)
|
||||
}
|
||||
|
||||
postable, err := ReceiverToPostableApiReceiver(rcv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert Receiver %q to APIReceiver: %w", rcv.Name, err)
|
||||
}
|
||||
encrypted[i] = postable
|
||||
}
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// RenameReceiverInRoutes renames all references to a receiver in routes. Returns number of routes that were updated
|
||||
func (rev *ConfigRevision) RenameReceiverInRoutes(oldName, newName string) int {
|
||||
return RenameReceiverInRoute(oldName, newName, rev.Config.AlertmanagerConfig.Route)
|
||||
|
||||
@@ -146,7 +146,7 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rcv, err := PostableApiReceiverToReceiver(postable, getReceiverProvenance(storedProvenances, postable))
|
||||
rcv, err := legacy_storage.PostableApiReceiverToReceiver(postable, legacy_storage.GetReceiverProvenance(storedProvenances, postable))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -206,7 +206,7 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivers, err := PostableApiReceiversToReceivers(postables, storedProvenances)
|
||||
receivers, err := legacy_storage.PostableApiReceiversToReceivers(postables, storedProvenances)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -280,7 +280,7 @@ func (rs *ReceiverService) ListReceivers(ctx context.Context, q models.ListRecei
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivers, err := PostableApiReceiversToReceivers(postables, storedProvenances)
|
||||
receivers, err := legacy_storage.PostableApiReceiversToReceivers(postables, storedProvenances)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -337,7 +337,7 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing, err := PostableApiReceiverToReceiver(postable, getReceiverProvenance(storedProvenances, postable))
|
||||
existing, err := legacy_storage.PostableApiReceiverToReceiver(postable, legacy_storage.GetReceiverProvenance(storedProvenances, postable))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err = PostableApiReceiverToReceiver(created, createdReceiver.Provenance)
|
||||
result, err = legacy_storage.PostableApiReceiverToReceiver(created, createdReceiver.Provenance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -480,7 +480,7 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing, err := PostableApiReceiverToReceiver(postable, getReceiverProvenance(storedProvenances, postable))
|
||||
existing, err := legacy_storage.PostableApiReceiverToReceiver(postable, legacy_storage.GetReceiverProvenance(storedProvenances, postable))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -567,7 +567,7 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := PostableApiReceiverToReceiver(updated, updatedReceiver.Provenance)
|
||||
result, err := legacy_storage.PostableApiReceiverToReceiver(updated, updatedReceiver.Provenance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -728,7 +728,7 @@ func TestReceiverService_Update(t *testing.T) {
|
||||
result, err := revision.CreateReceiver(tc.existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
created, err := PostableApiReceiverToReceiver(result, tc.existing.Provenance)
|
||||
created, err := legacy_storage.PostableApiReceiverToReceiver(result, tc.existing.Provenance)
|
||||
require.NoError(t, err)
|
||||
err = sut.cfgStore.Save(context.Background(), revision, tc.user.GetOrgID())
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
remoteClient "github.com/grafana/grafana/pkg/services/ngalert/remote/client"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/sender"
|
||||
)
|
||||
@@ -280,22 +281,22 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config
|
||||
if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil {
|
||||
return err
|
||||
}
|
||||
rawDecrypted, configHash, err := am.decryptConfiguration(ctx, c)
|
||||
decryptedCfg, err := am.decryptConfiguration(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawDecrypted, err := json.Marshal(decryptedCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal decrypted configuration: %w", err)
|
||||
}
|
||||
configHash := md5.Sum(rawDecrypted)
|
||||
|
||||
// Send the configuration only if we need to.
|
||||
if !am.shouldSendConfig(ctx, configHash) {
|
||||
return nil
|
||||
}
|
||||
|
||||
decrypted, err := notifier.Load(rawDecrypted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.sendConfiguration(ctx, decrypted, config.ConfigurationHash, config.CreatedAt, am.isDefaultConfiguration(configHash))
|
||||
return am.sendConfiguration(ctx, decryptedCfg, config.ConfigurationHash, config.CreatedAt, am.isDefaultConfiguration(configHash))
|
||||
}
|
||||
|
||||
func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool {
|
||||
@@ -305,38 +306,39 @@ func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool {
|
||||
// decryptConfiguration creates a copy of the configuration, decrypts it, and returns the decrypted configuration alongside its hash.
|
||||
// Should not be used outside of this package and the specific use case of decrypting the configuration before sending
|
||||
// it to the remote Alertmanager.
|
||||
func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels.PostableUserConfig) ([]byte, [16]byte, error) {
|
||||
fn := func(payload []byte) ([]byte, error) {
|
||||
return am.decrypt(ctx, payload)
|
||||
}
|
||||
|
||||
func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels.PostableUserConfig) (*apimodels.PostableUserConfig, error) {
|
||||
// Create a copy of the configuration to avoid modifying the original
|
||||
cfgCopy := &apimodels.PostableUserConfig{}
|
||||
rawCfg, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, [16]byte{}, fmt.Errorf("unable to marshal original configuration: %w", err)
|
||||
return nil, fmt.Errorf("unable to marshal original configuration: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(rawCfg, cfgCopy); err != nil {
|
||||
return nil, [16]byte{}, fmt.Errorf("unable to unmarshal original configuration: %w", err)
|
||||
return nil, fmt.Errorf("unable to unmarshal original configuration: %w", err)
|
||||
}
|
||||
|
||||
// Iterate through receivers and decrypt secure settings on the copy
|
||||
for _, rcv := range cfgCopy.AlertmanagerConfig.Receivers {
|
||||
for _, gmr := range rcv.GrafanaManagedReceivers {
|
||||
decrypted, err := gmr.DecryptSecureSettings(fn)
|
||||
if err != nil {
|
||||
return nil, [16]byte{}, fmt.Errorf("unable to decrypt settings on receiver %q (uid: %q): %w", gmr.Name, gmr.UID, err)
|
||||
}
|
||||
gmr.SecureSettings = decrypted
|
||||
}
|
||||
}
|
||||
|
||||
rawDecrypted, err := json.Marshal(cfgCopy)
|
||||
// Decrypt the receivers in the configuration.
|
||||
decryptedReceivers, err := legacy_storage.DecryptedReceivers(cfgCopy.AlertmanagerConfig.Receivers, decrypter(ctx, am.decrypt))
|
||||
if err != nil {
|
||||
return nil, [16]byte{}, fmt.Errorf("unable to marshal decrypted configuration: %w", err)
|
||||
return nil, fmt.Errorf("unable to decrypt receivers: %w", err)
|
||||
}
|
||||
cfgCopy.AlertmanagerConfig.Receivers = decryptedReceivers
|
||||
|
||||
return rawDecrypted, md5.Sum(rawDecrypted), nil
|
||||
return cfgCopy, nil
|
||||
}
|
||||
|
||||
func decrypter(ctx context.Context, decryptFn DecryptFn) models.DecryptFn {
|
||||
return func(value string) (string, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
decrypted, err := decryptFn(ctx, decoded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decrypted), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (am *Alertmanager) sendConfiguration(ctx context.Context, decrypted *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error {
|
||||
@@ -388,17 +390,12 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P
|
||||
if err := am.autogenFn(ctx, am.log, am.orgID, &cfg.AlertmanagerConfig, false); err != nil {
|
||||
return err
|
||||
}
|
||||
rawDecrypted, _, err := am.decryptConfiguration(ctx, cfg)
|
||||
decryptedCfg, err := am.decryptConfiguration(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decrypted, err := notifier.Load(rawDecrypted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.sendConfiguration(ctx, decrypted, hash, time.Now().Unix(), false)
|
||||
return am.sendConfiguration(ctx, decryptedCfg, hash, time.Now().Unix(), false)
|
||||
}
|
||||
|
||||
// SaveAndApplyDefaultConfig sends the default Grafana Alertmanager configuration to the remote Alertmanager.
|
||||
@@ -412,19 +409,14 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error {
|
||||
if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil {
|
||||
return err
|
||||
}
|
||||
rawDecrypted, _, err := am.decryptConfiguration(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decrypted, err := notifier.Load(rawDecrypted)
|
||||
decryptedCfg, err := am.decryptConfiguration(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.sendConfiguration(
|
||||
ctx,
|
||||
decrypted,
|
||||
decryptedCfg,
|
||||
am.defaultConfigHash,
|
||||
time.Now().Unix(),
|
||||
true,
|
||||
@@ -581,34 +573,14 @@ func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver,
|
||||
}
|
||||
|
||||
func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) {
|
||||
fn := func(payload []byte) ([]byte, error) {
|
||||
return am.decrypt(ctx, payload)
|
||||
decryptedReceivers, err := legacy_storage.DecryptedReceivers(c.Receivers, decrypter(ctx, am.decrypt))
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to decrypt receivers: %w", err)
|
||||
}
|
||||
|
||||
receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers))
|
||||
for _, r := range c.Receivers {
|
||||
integrations := make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers))
|
||||
for _, gr := range r.GrafanaManagedReceivers {
|
||||
decrypted, err := gr.DecryptSecureSettings(fn)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
integrations = append(integrations, &alertingNotify.GrafanaIntegrationConfig{
|
||||
UID: gr.UID,
|
||||
Name: gr.Name,
|
||||
Type: gr.Type,
|
||||
DisableResolveMessage: gr.DisableResolveMessage,
|
||||
Settings: json.RawMessage(gr.Settings),
|
||||
SecureSettings: decrypted,
|
||||
})
|
||||
}
|
||||
receivers = append(receivers, &alertingNotify.APIReceiver{
|
||||
ConfigReceiver: r.Receiver,
|
||||
GrafanaIntegrations: alertingNotify.GrafanaIntegrations{
|
||||
Integrations: integrations,
|
||||
},
|
||||
})
|
||||
apiReceivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers))
|
||||
for _, r := range decryptedReceivers {
|
||||
apiReceivers = append(apiReceivers, notifier.PostableApiReceiverToApiReceiver(r))
|
||||
}
|
||||
var alert *alertingNotify.TestReceiversConfigAlertParams
|
||||
if c.Alert != nil {
|
||||
@@ -617,7 +589,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
|
||||
|
||||
return am.mimirClient.TestReceivers(ctx, alertingNotify.TestReceiversConfigBodyParams{
|
||||
Alert: alert,
|
||||
Receivers: receivers,
|
||||
Receivers: apiReceivers,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/remote/client"
|
||||
ngfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
@@ -44,12 +45,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
testPasswordBase64 = base64.StdEncoding.EncodeToString([]byte(testPassword))
|
||||
defaultGrafanaConfig = setting.GetAlertmanagerDefaultConfiguration()
|
||||
errTest = errors.New("test")
|
||||
|
||||
// Valid Grafana Alertmanager configuration with secret in base64.
|
||||
testGrafanaConfigWithEncryptedSecret = fmt.Sprintf(`{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test"},"secureSettings":{"password":"%s"}}]}]}}`, testPasswordBase64)
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -57,7 +54,7 @@ const (
|
||||
|
||||
// Valid Grafana Alertmanager configurations.
|
||||
testGrafanaConfig = `{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"","name":"some other name","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"}}]}]}}`
|
||||
testGrafanaConfigWithSecret = `{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test"},"secureSettings":{"password":"test"}}]}]}}`
|
||||
testGrafanaConfigWithSecret = `{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test","password":"test"}}]}]}}`
|
||||
testGrafanaDefaultConfigWithDifferentFieldOrder = `{"alertmanager_config":{"route":{"group_by":["alertname","grafana_folder"],"receiver":"grafana-default-email"},"receivers":[{"grafana_managed_receiver_configs":[{"uid":"","name":"email receiver","type":"email","settings":{"addresses":"<example@email.com>"}}],"name":"grafana-default-email"}]}}`
|
||||
|
||||
// Valid Alertmanager state base64 encoded.
|
||||
@@ -167,9 +164,14 @@ func TestApplyConfig(t *testing.T) {
|
||||
var c apimodels.PostableUserConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &c))
|
||||
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
|
||||
err := notifier.EncryptReceiverConfigs(c.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
return secretsService.Encrypt(ctx, payload, secrets.WithoutScope())
|
||||
encryptedReceivers, err := legacy_storage.EncryptedReceivers(c.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
})
|
||||
c.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||
require.NoError(t, err)
|
||||
|
||||
// The encrypted configuration should be different than the one we will send.
|
||||
@@ -289,15 +291,7 @@ func TestApplyConfig(t *testing.T) {
|
||||
|
||||
func TestCompareAndSendConfiguration(t *testing.T) {
|
||||
const tenantID = "test"
|
||||
cfgWithSecret, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
|
||||
require.NoError(t, err)
|
||||
testValue := []byte("test")
|
||||
decryptFn := func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if string(payload) == string(testValue) {
|
||||
return testValue, nil
|
||||
}
|
||||
return nil, errTest
|
||||
}
|
||||
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
|
||||
|
||||
var got string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -333,62 +327,90 @@ func TestCompareAndSendConfiguration(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a config with correctly encrypted and encoded secrets.
|
||||
var inputCfg apimodels.PostableUserConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
|
||||
encryptedReceivers, err := legacy_storage.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
})
|
||||
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||
require.NoError(t, err)
|
||||
testGrafanaConfigWithEncryptedSecret, err := json.Marshal(inputCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Created a config with invalid base64 encoding in the secret.
|
||||
inputCfg.AlertmanagerConfig.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers[0].SecureSettings["password"] = "!"
|
||||
testGrafanaConfigWithBadEncoding, err := json.Marshal(inputCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a config with a valid base64 encoding but an invalid encryption.
|
||||
inputCfg.AlertmanagerConfig.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers[0].SecureSettings["password"] = base64.StdEncoding.EncodeToString([]byte("test"))
|
||||
testGrafanaConfigWithBadEncryption, err := json.Marshal(inputCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfgWithDecryptedSecret, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
|
||||
require.NoError(t, err)
|
||||
|
||||
cfgWithAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testAutogenFn(nil, nil, 0, &cfgWithAutogenRoutes.AlertmanagerConfig, false))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
autogenFn AutogenFn
|
||||
expCfg *client.UserGrafanaConfig
|
||||
expErr string
|
||||
name string
|
||||
config string
|
||||
autogenFn AutogenFn
|
||||
expCfg *client.UserGrafanaConfig
|
||||
expErrContains []string
|
||||
}{
|
||||
{
|
||||
"invalid config",
|
||||
"{}",
|
||||
NoopAutogenFn,
|
||||
nil,
|
||||
"unable to parse Alertmanager configuration: no route provided in config",
|
||||
[]string{"no route provided in config"},
|
||||
},
|
||||
{
|
||||
"invalid base-64 in key",
|
||||
strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, `"password":"!"`, 1),
|
||||
string(testGrafanaConfigWithBadEncoding),
|
||||
NoopAutogenFn,
|
||||
nil,
|
||||
`unable to decrypt settings on receiver "WH" (uid: "dde6ntuob69dtf"): failed to decode value for key 'password': illegal base64 data at input byte 0`,
|
||||
[]string{`"grafana-default-email"`, "dde6ntuob69dtf", "password", "illegal base64 data at input byte 0"},
|
||||
},
|
||||
{
|
||||
"decrypt error",
|
||||
testGrafanaConfigWithSecret,
|
||||
string(testGrafanaConfigWithBadEncryption),
|
||||
NoopAutogenFn,
|
||||
nil,
|
||||
fmt.Sprintf(`unable to decrypt settings on receiver "WH" (uid: "dde6ntuob69dtf"): failed to decrypt value for key 'password': %s`, errTest.Error()),
|
||||
[]string{`"grafana-default-email"`, "dde6ntuob69dtf", "password", "unable to compute salt"},
|
||||
},
|
||||
{
|
||||
"error from autogen function",
|
||||
strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, fmt.Sprintf("%q:%q", "password", base64.StdEncoding.EncodeToString(testValue)), 1),
|
||||
string(testGrafanaConfigWithEncryptedSecret),
|
||||
errAutogenFn,
|
||||
nil,
|
||||
errTest.Error(),
|
||||
[]string{errTest.Error()},
|
||||
},
|
||||
{
|
||||
"no error",
|
||||
strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, fmt.Sprintf("%q:%q", "password", base64.StdEncoding.EncodeToString(testValue)), 1),
|
||||
string(testGrafanaConfigWithEncryptedSecret),
|
||||
NoopAutogenFn,
|
||||
&client.UserGrafanaConfig{
|
||||
GrafanaAlertmanagerConfig: cfgWithSecret,
|
||||
GrafanaAlertmanagerConfig: cfgWithDecryptedSecret,
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"no error, with auto-generated routes",
|
||||
strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, fmt.Sprintf("%q:%q", "password", base64.StdEncoding.EncodeToString(testValue)), 1),
|
||||
string(testGrafanaConfigWithEncryptedSecret),
|
||||
testAutogenFn,
|
||||
&client.UserGrafanaConfig{
|
||||
GrafanaAlertmanagerConfig: cfgWithAutogenRoutes,
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -398,7 +420,7 @@ func TestCompareAndSendConfiguration(t *testing.T) {
|
||||
am, err := NewAlertmanager(ctx,
|
||||
cfg,
|
||||
fstore,
|
||||
decryptFn,
|
||||
secretsService.Decrypt,
|
||||
NoopAutogenFn,
|
||||
m,
|
||||
tracing.InitializeTracerForTest(),
|
||||
@@ -413,29 +435,23 @@ func TestCompareAndSendConfiguration(t *testing.T) {
|
||||
AlertmanagerConfiguration: test.config,
|
||||
}
|
||||
err = am.CompareAndSendConfiguration(ctx, &cfg)
|
||||
if test.expErr == "" {
|
||||
if len(test.expErrContains) == 0 {
|
||||
require.NoError(tt, err)
|
||||
rawCfg, err := json.Marshal(test.expCfg)
|
||||
require.NoError(tt, err)
|
||||
require.JSONEq(tt, string(rawCfg), got)
|
||||
return
|
||||
}
|
||||
require.Equal(tt, test.expErr, err.Error())
|
||||
for _, expErr := range test.expErrContains {
|
||||
require.ErrorContains(tt, err, expErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_TestReceiversDecryptsSecureSettings(t *testing.T) {
|
||||
const tenantID = "test"
|
||||
const testKey = "test-key"
|
||||
const testValue = "test-value"
|
||||
decryptFn := func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if string(payload) == testValue {
|
||||
return []byte(testValue), nil
|
||||
}
|
||||
return nil, errTest
|
||||
}
|
||||
|
||||
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
|
||||
var got apimodels.TestReceiversConfigBodyParams
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader))
|
||||
@@ -459,33 +475,40 @@ func Test_TestReceiversDecryptsSecureSettings(t *testing.T) {
|
||||
am, err := NewAlertmanager(context.Background(),
|
||||
cfg,
|
||||
fstore,
|
||||
decryptFn,
|
||||
secretsService.Decrypt,
|
||||
NoopAutogenFn,
|
||||
m,
|
||||
tracing.InitializeTracerForTest(),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
var inputCfg apimodels.PostableUserConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
|
||||
encryptedReceivers, err := legacy_storage.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
})
|
||||
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||
require.NoError(t, err)
|
||||
|
||||
params := apimodels.TestReceiversConfigBodyParams{
|
||||
Alert: &apimodels.TestReceiversConfigAlertParams{},
|
||||
Receivers: []*definition.PostableApiReceiver{
|
||||
{
|
||||
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{
|
||||
{
|
||||
SecureSettings: map[string]string{
|
||||
testKey: base64.StdEncoding.EncodeToString([]byte(testValue)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Alert: &apimodels.TestReceiversConfigAlertParams{},
|
||||
Receivers: inputCfg.AlertmanagerConfig.Receivers,
|
||||
}
|
||||
|
||||
_, _, err = am.TestReceivers(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{testKey: testValue}, got.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers[0].SecureSettings)
|
||||
|
||||
expectedSettings, err := json.Marshal(map[string]any{
|
||||
"url": "http://localhost:8080",
|
||||
"username": "test",
|
||||
"password": testPassword,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expectedSettings, got.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers[0].Settings)
|
||||
}
|
||||
|
||||
func Test_isDefaultConfiguration(t *testing.T) {
|
||||
@@ -533,44 +556,6 @@ func Test_isDefaultConfiguration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptConfiguration(t *testing.T) {
|
||||
t.Run("should not modify the original config", func(t *testing.T) {
|
||||
var inputCfg apimodels.PostableUserConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithEncryptedSecret), &inputCfg))
|
||||
|
||||
decryptFn := func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if string(payload) == testPassword {
|
||||
return []byte(testPassword), nil
|
||||
}
|
||||
return nil, fmt.Errorf("incorrect payload")
|
||||
}
|
||||
|
||||
am := &Alertmanager{
|
||||
decrypt: decryptFn,
|
||||
}
|
||||
|
||||
rawDecrypted, _, err := am.decryptConfiguration(context.Background(), &inputCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
currentJSON, err := json.Marshal(inputCfg)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, testGrafanaConfigWithEncryptedSecret, string(currentJSON), "Original configuration should not be modified")
|
||||
|
||||
var decryptedCfg apimodels.PostableUserConfig
|
||||
require.NoError(t, json.Unmarshal(rawDecrypted, &decryptedCfg))
|
||||
|
||||
found := false
|
||||
for _, rcv := range decryptedCfg.AlertmanagerConfig.Receivers {
|
||||
for _, gmr := range rcv.GrafanaManagedReceivers {
|
||||
if gmr.Type == "webhook" && gmr.SecureSettings["password"] == testPassword {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, found, "Decrypted configuration should contain decrypted password")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
@@ -684,9 +669,14 @@ func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) {
|
||||
{
|
||||
postableCfg, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
|
||||
require.NoError(t, err)
|
||||
err = notifier.EncryptReceiverConfigs(postableCfg.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
return secretsService.Encrypt(ctx, payload, secrets.WithoutScope())
|
||||
encryptedReceivers, err := legacy_storage.EncryptedReceivers(postableCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
})
|
||||
postableCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||
require.NoError(t, err)
|
||||
|
||||
// The encrypted configuration should be different than the one we will send.
|
||||
@@ -698,6 +688,11 @@ func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, am.SaveAndApplyConfig(ctx, postableCfg))
|
||||
|
||||
// Check that the original configuration is not modified (decrypted).
|
||||
currentJSON, err := json.Marshal(postableCfg)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, string(encryptedConfig), string(currentJSON), "Original configuration should not be modified")
|
||||
|
||||
// Check that the configuration was uploaded to the remote Alertmanager.
|
||||
config, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user