diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index ed064df3ff5..1787dcfa5e9 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -609,7 +609,20 @@ func createMultiOrgAlertmanager(t *testing.T, configs map[int64]*ngmodels.AlertC }, // do not poll in tests. } - mam, err := notifier.NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, provStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService, featuremgmt.WithManager(featuremgmt.FlagAlertingSimplifiedRouting)) + mam, err := notifier.NewMultiOrgAlertmanager( + cfg, + configStore, + orgStore, + kvStore, + provStore, + decryptFn, + m.GetMultiOrgAlertmanagerMetrics(), + nil, + ngfakes.NewFakeReceiverPermissionsService(), + log.New("testlogger"), + secretsService, + featuremgmt.WithManager(featuremgmt.FlagAlertingSimplifiedRouting), + ) require.NoError(t, err) err = mam.LoadAndSyncAlertmanagersForOrgs(context.Background()) require.NoError(t, err) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 74ca6331bcd..b2628d6ae08 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -311,7 +311,21 @@ func (ng *AlertNG) init() error { decryptFn := ng.SecretsService.GetDecryptedValue multiOrgMetrics := ng.Metrics.GetMultiOrgAlertmanagerMetrics() - moa, err := notifier.NewMultiOrgAlertmanager(ng.Cfg, ng.store, ng.store, ng.KVStore, ng.store, decryptFn, multiOrgMetrics, ng.NotificationService, moaLogger, ng.SecretsService, ng.FeatureToggles, overrides...) + moa, err := notifier.NewMultiOrgAlertmanager( + ng.Cfg, + ng.store, + ng.store, + ng.KVStore, + ng.store, + decryptFn, + multiOrgMetrics, + ng.NotificationService, + ng.ResourcePermissions, + moaLogger, + ng.SecretsService, + ng.FeatureToggles, + overrides..., + ) if err != nil { return err } diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 7d71de2c343..c2248fe91c6 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -2,16 +2,19 @@ package notifier import ( "context" + "encoding/json" "errors" "fmt" "time" "github.com/go-openapi/strfmt" + "k8s.io/apimachinery/pkg/util/sets" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/services/featuremgmt" "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" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" ) @@ -58,10 +61,32 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyDefaultConfig(ctx context.Context, return err } + previousConfig, cleanPermissionsErr := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, orgId) + err = orgAM.SaveAndApplyDefaultConfig(ctx) if err != nil { return err } + + // Attempt to cleanup permissions for receivers that are no longer defined and add defaults for new receivers. + // Failure should not prevent the default config from being applied. + if cleanPermissionsErr == nil { + cleanPermissionsErr = func() error { + defaultedConfig, err := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, orgId) + if err != nil { + return err + } + newReceiverNames, err := extractReceiverNames(defaultedConfig.AlertmanagerConfiguration) + if err != nil { + return err + } + return moa.cleanPermissions(ctx, orgId, previousConfig, newReceiverNames) + }() + } + if cleanPermissionsErr != nil { + moa.logger.Error("Failed to clean permissions for receivers", "error", cleanPermissionsErr) + } + return nil } @@ -130,12 +155,29 @@ func (moa *MultiOrgAlertmanager) ActivateHistoricalConfiguration(ctx context.Con } } + previousConfig, cleanPermissionsErr := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, orgId) + if err := am.SaveAndApplyConfig(ctx, cfg); err != nil { moa.logger.Error("Unable to save and apply historical alertmanager configuration", "error", err, "org", orgId, "id", id) return AlertmanagerConfigRejectedError{err} } moa.logger.Info("Applied historical alertmanager configuration", "org", orgId, "id", id) + // Attempt to cleanup permissions for receivers that are no longer defined and add defaults for new receivers. + // Failure should not prevent the default config from being applied. + if cleanPermissionsErr == nil { + cleanPermissionsErr = func() error { + newReceiverNames, err := extractReceiverNames(config.AlertmanagerConfiguration) + if err != nil { + return err + } + return moa.cleanPermissions(ctx, orgId, previousConfig, newReceiverNames) + }() + } + if cleanPermissionsErr != nil { + moa.logger.Error("Failed to clean permissions for receivers", "error", cleanPermissionsErr) + } + return nil } @@ -231,13 +273,14 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyAlertmanagerConfiguration(ctx conte } // Get the last known working configuration - _, err := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, org) + previousConfig, err := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, org) if err != nil { // If we don't have a configuration there's nothing for us to know and we should just continue saving the new one if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return fmt.Errorf("failed to get latest configuration %w", err) } } + cleanPermissionsErr := err if err := moa.Crypto.ProcessSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers); err != nil { return fmt.Errorf("failed to post process Alertmanager configuration: %w", err) @@ -268,6 +311,21 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyAlertmanagerConfiguration(ctx conte return AlertmanagerConfigRejectedError{err} } + // Attempt to cleanup permissions for receivers that are no longer defined and add defaults for new receivers. + // Failure should not prevent the default config from being applied. + if cleanPermissionsErr == nil { + cleanPermissionsErr = func() error { + newReceiverNames := make(sets.Set[string], len(config.AlertmanagerConfig.Receivers)) + for _, r := range config.AlertmanagerConfig.Receivers { + newReceiverNames.Insert(r.Name) + } + return moa.cleanPermissions(ctx, org, previousConfig, newReceiverNames) + }() + } + if cleanPermissionsErr != nil { + moa.logger.Error("Failed to clean permissions for receivers", "error", cleanPermissionsErr) + } + return nil } @@ -352,3 +410,51 @@ func (moa *MultiOrgAlertmanager) mergeProvenance(ctx context.Context, config def return config, nil } + +// cleanPermissions will remove permissions for receivers that are no longer defined in the new configuration and +// set default permissions for new receivers. +func (moa *MultiOrgAlertmanager) cleanPermissions(ctx context.Context, orgID int64, previousConfig *models.AlertConfiguration, newReceiverNames sets.Set[string]) error { + previousReceiverNames, err := extractReceiverNames(previousConfig.AlertmanagerConfiguration) + if err != nil { + return fmt.Errorf("failed to extract receiver names from previous configuration: %w", err) + } + + var errs []error + for receiverName := range previousReceiverNames.Difference(newReceiverNames) { // Deleted receivers. + if err := moa.receiverResourcePermissions.DeleteResourcePermissions(ctx, orgID, legacy_storage.NameToUid(receiverName)); err != nil { + errs = append(errs, fmt.Errorf("failed to delete permissions for receiver %s: %w", receiverName, err)) + } + } + + for receiverName := range newReceiverNames.Difference(previousReceiverNames) { // Added receivers. + moa.receiverResourcePermissions.SetDefaultPermissions(ctx, orgID, nil, legacy_storage.NameToUid(receiverName)) + } + + return errors.Join(errs...) +} + +// extractReceiverNames extracts receiver names from the raw Alertmanager configuration. Unmarshalling ignores fields +// unrelated to receiver names, making it more resilient to invalid configurations. +func extractReceiverNames(rawConfig string) (sets.Set[string], error) { + // Slimmed down version of the Alertmanager configuration to extract receiver names. This is more resilient to + // invalid configurations when all we are interested in is the receiver names. + type receiverUserConfig struct { + AlertmanagerConfig struct { + Receivers []struct { + Name string `yaml:"name" json:"name"` + } `yaml:"receivers,omitempty" json:"receivers,omitempty"` + } `yaml:"alertmanager_config" json:"alertmanager_config"` + } + + cfg := &receiverUserConfig{} + if err := json.Unmarshal([]byte(rawConfig), cfg); err != nil { + return nil, fmt.Errorf("unable to parse Alertmanager configuration: %w", err) + } + + receiverNames := make(sets.Set[string], len(cfg.AlertmanagerConfig.Receivers)) + for _, r := range cfg.AlertmanagerConfig.Receivers { + receiverNames[r.Name] = struct{}{} + } + + return receiverNames, nil +} diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index 464c1036296..71140200558 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -12,6 +12,7 @@ import ( alertingCluster "github.com/grafana/alerting/cluster" "github.com/grafana/grafana/pkg/apimachinery/errutil" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" alertingNotify "github.com/grafana/alerting/notify" @@ -100,6 +101,8 @@ type MultiOrgAlertmanager struct { metrics *metrics.MultiOrgAlertmanager ns notifications.Service + + receiverResourcePermissions ac.ReceiverPermissionsService } type OrgAlertmanagerFactory func(ctx context.Context, orgID int64) (Alertmanager, error) @@ -121,6 +124,7 @@ func NewMultiOrgAlertmanager( decryptFn alertingNotify.GetDecryptedValueFn, m *metrics.MultiOrgAlertmanager, ns notifications.Service, + receiverResourcePermissions ac.ReceiverPermissionsService, l log.Logger, s secrets.Service, featureManager featuremgmt.FeatureToggles, @@ -130,17 +134,18 @@ func NewMultiOrgAlertmanager( Crypto: NewCrypto(s, configStore, l), ProvStore: provStore, - logger: l, - settings: cfg, - featureManager: featureManager, - alertmanagers: map[int64]Alertmanager{}, - configStore: configStore, - orgStore: orgStore, - kvStore: kvStore, - decryptFn: decryptFn, - metrics: m, - ns: ns, - peer: &NilPeer{}, + logger: l, + settings: cfg, + featureManager: featureManager, + alertmanagers: map[int64]Alertmanager{}, + configStore: configStore, + orgStore: orgStore, + kvStore: kvStore, + decryptFn: decryptFn, + receiverResourcePermissions: receiverResourcePermissions, + metrics: m, + ns: ns, + peer: &NilPeer{}, } if cfg.UnifiedAlerting.SkipClustering { diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go index 81573c42061..5301b37bf9a 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go @@ -98,6 +98,7 @@ func TestMultiorgAlertmanager_RemoteSecondaryMode(t *testing.T) { secretsService.GetDecryptedValue, m.GetMultiOrgAlertmanagerMetrics(), nil, + ngfakes.NewFakeReceiverPermissionsService(), nopLogger, secretsService, featuremgmt.WithFeatures(), diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go index a2fb2647ace..e53da1fa451 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go @@ -383,7 +383,20 @@ func setupMam(t *testing.T, cfg *setting.Cfg) *MultiOrgAlertmanager { decryptFn := secretsService.GetDecryptedValue reg := prometheus.NewPedanticRegistry() m := metrics.NewNGAlert(reg) - mam, err := NewMultiOrgAlertmanager(cfg, cs, orgStore, kvStore, provStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService, featuremgmt.WithFeatures()) + mam, err := NewMultiOrgAlertmanager( + cfg, + cs, + orgStore, + kvStore, + provStore, + decryptFn, + m.GetMultiOrgAlertmanagerMetrics(), + nil, + ngfakes.NewFakeReceiverPermissionsService(), + log.New("testlogger"), + secretsService, + featuremgmt.WithFeatures(), + ) require.NoError(t, err) return mam } diff --git a/pkg/services/ngalert/sender/router_test.go b/pkg/services/ngalert/sender/router_test.go index c10f88e6001..25f0c7566aa 100644 --- a/pkg/services/ngalert/sender/router_test.go +++ b/pkg/services/ngalert/sender/router_test.go @@ -491,7 +491,20 @@ func createMultiOrgAlertmanager(t *testing.T, orgs []int64) *notifier.MultiOrgAl m := metrics.NewNGAlert(registry) secretsService := secretsManager.SetupTestService(t, fake_secrets.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - moa, err := notifier.NewMultiOrgAlertmanager(cfg, cfgStore, orgStore, kvStore, fakes.NewFakeProvisioningStore(), decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService, featuremgmt.WithFeatures()) + moa, err := notifier.NewMultiOrgAlertmanager( + cfg, + cfgStore, + orgStore, + kvStore, + fakes.NewFakeProvisioningStore(), + decryptFn, + m.GetMultiOrgAlertmanagerMetrics(), + nil, + fakes.NewFakeReceiverPermissionsService(), + log.New("testlogger"), + secretsService, + featuremgmt.WithFeatures(), + ) require.NoError(t, err) require.NoError(t, moa.LoadAndSyncAlertmanagersForOrgs(context.Background())) require.Eventually(t, func() bool {