Skip to content

Move and integrate Receiver configuration#484

Open
yuri-tceretian wants to merge 4 commits intomainfrom
yuri-tseretyan/move-v0-integrations
Open

Move and integrate Receiver configuration#484
yuri-tceretian wants to merge 4 commits intomainfrom
yuri-tseretyan/move-v0-integrations

Conversation

@yuri-tceretian
Copy link
Collaborator

@yuri-tceretian yuri-tceretian commented Feb 12, 2026

Description

This pull request includes the following changes:

  • Relocated Receiver component to align with the updated architecture.
  • Updated all version 0 configurations to use the relocated HTTPClientConfig.
  • Added compatibility functions to the copied HTTPClientConfig.
  • Integrated receiver configurations and implementation migrated from the fork.

Checklist

  • Documentation has been updated, if necessary.
  • Unit and integration tests have been added or updated.
  • Changes have been tested locally.

Note

Medium Risk
Touches core alerting receiver config types and integration instantiation paths; mis-mapped HTTPConfig defaults or config unmarshalling differences could break notification delivery across multiple integrations.

Overview
Switches the API/config model from upstream config.Receiver to a new definition.Receiver that embeds Grafana-maintained v0mimir1 receiver config types (Discord/Email/Slack/Webhook/etc., plus Jira) and enforces receiver-name validation during YAML unmarshalling.

Introduces http/v0mimir1 with a standalone HTTPClientConfig plus conversion helpers to/from prometheus/common configs, then updates compat loading/validation to populate each integration’s HTTPConfig via this conversion.

Updates notifier construction to use the Grafana v0mimir1 integration implementations (and adds Jira to BuildPrometheusReceiverIntegrations), removes the now-unused GrafanaToUpstreamConfig conversion path, and refactors tests/schema mapping to the new types and secrets mapping logic.

Written by Cursor Bugbot for commit d56bd27. This will update automatically on new commits. Configure here.

@yuri-tceretian yuri-tceretian requested a review from a team as a code owner February 12, 2026 21:32
@github-project-automation github-project-automation bot moved this to In review in Alerting Feb 12, 2026
@yuri-tceretian yuri-tceretian force-pushed the yuri-tseretyan/move-v0-integrations branch from 083b53b to d56bd27 Compare February 12, 2026 21:35
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issue.

MSTeamsConfigs []*teams_v0mimir1.Config `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
MSTeamsV2Configs []*teams_v0mimir2.Config `yaml:"msteamsv2_configs,omitempty" json:"msteamsv2_configs,omitempty"`
JiraConfigs []*jira_v0mimir1.Config `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Receiver type changes silently bypass Mimir validation

High Severity

The new Receiver struct uses v0mimir1.*Config types (e.g., discord_v0mimir1.Config, email_v0mimir1.Config), but ValidateAlertmanagerConfig in mimir_validation.go uses reflection to match against the old upstream types (e.g., config.DiscordConfig, config.EmailConfig, commoncfg.HTTPClientConfig). Since these are different types, none of the switch cases match when recursively walking the new Receiver, silently skipping all Mimir-specific validation. This bypasses security-relevant checks that prevent file-based credentials, disallowed proxy settings, and TLS file paths.

Fix in Cursor Fix in Web

@cursor
Copy link

cursor bot commented Feb 12, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Receiver type changes silently bypass Mimir validation
    • Added switch cases and validation functions for all v0mimir1 config types so ValidateAlertmanagerConfig now properly validates the new receiver types.

Create PR

Or push these changes by commenting:

@cursor push 6226a16f55
Preview (6226a16f55)
diff --git a/definition/mimir_validation.go b/definition/mimir_validation.go
--- a/definition/mimir_validation.go
+++ b/definition/mimir_validation.go
@@ -8,6 +8,19 @@
 
 	"github.com/prometheus/alertmanager/config"
 	commoncfg "github.com/prometheus/common/config"
+
+	httpcfg "github.com/grafana/alerting/http/v0mimir1"
+	discord_v0mimir1 "github.com/grafana/alerting/receivers/discord/v0mimir1"
+	email_v0mimir1 "github.com/grafana/alerting/receivers/email/v0mimir1"
+	opsgenie_v0mimir1 "github.com/grafana/alerting/receivers/opsgenie/v0mimir1"
+	pagerduty_v0mimir1 "github.com/grafana/alerting/receivers/pagerduty/v0mimir1"
+	pushover_v0mimir1 "github.com/grafana/alerting/receivers/pushover/v0mimir1"
+	slack_v0mimir1 "github.com/grafana/alerting/receivers/slack/v0mimir1"
+	teams_v0mimir1 "github.com/grafana/alerting/receivers/teams/v0mimir1"
+	teams_v0mimir2 "github.com/grafana/alerting/receivers/teams/v0mimir2"
+	telegram_v0mimir1 "github.com/grafana/alerting/receivers/telegram/v0mimir1"
+	victorops_v0mimir1 "github.com/grafana/alerting/receivers/victorops/v0mimir1"
+	webhook_v0mimir1 "github.com/grafana/alerting/receivers/webhook/v0mimir1"
 )
 
 var (
@@ -118,6 +131,72 @@
 		if err := validateWebhookConfig(v.Interface().(config.WebhookConfig)); err != nil {
 			return err
 		}
+
+	// v0mimir1 types
+	case reflect.TypeOf(httpcfg.HTTPClientConfig{}):
+		if err := validateV0Mimir1HTTPConfig(v.Interface().(httpcfg.HTTPClientConfig)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(httpcfg.TLSConfig{}):
+		if err := validateV0Mimir1TLSConfig(v.Interface().(httpcfg.TLSConfig)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(discord_v0mimir1.Config{}):
+		if err := validateV0Mimir1DiscordConfig(v.Interface().(discord_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(email_v0mimir1.Config{}):
+		if err := validateV0Mimir1EmailConfig(v.Interface().(email_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(slack_v0mimir1.Config{}):
+		if err := validateV0Mimir1SlackConfig(v.Interface().(slack_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(opsgenie_v0mimir1.Config{}):
+		if err := validateV0Mimir1OpsGenieConfig(v.Interface().(opsgenie_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(victorops_v0mimir1.Config{}):
+		if err := validateV0Mimir1VictorOpsConfig(v.Interface().(victorops_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(pagerduty_v0mimir1.Config{}):
+		if err := validateV0Mimir1PagerDutyConfig(v.Interface().(pagerduty_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(pushover_v0mimir1.Config{}):
+		if err := validateV0Mimir1PushoverConfig(v.Interface().(pushover_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(telegram_v0mimir1.Config{}):
+		if err := validateV0Mimir1TelegramConfig(v.Interface().(telegram_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(webhook_v0mimir1.Config{}):
+		if err := validateV0Mimir1WebhookConfig(v.Interface().(webhook_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(teams_v0mimir1.Config{}):
+		if err := validateV0Mimir1MSTeamsConfig(v.Interface().(teams_v0mimir1.Config)); err != nil {
+			return err
+		}
+
+	case reflect.TypeOf(teams_v0mimir2.Config{}):
+		if err := validateV0Mimir2MSTeamsConfig(v.Interface().(teams_v0mimir2.Config)); err != nil {
+			return err
+		}
 	}
 
 	// If the input config is a struct, recursively iterate on all fields.
@@ -325,3 +404,145 @@
 	}
 	return nil
 }
+
+// validateV0Mimir1HTTPConfig validates the v0mimir1 HTTP config and returns an error if it contains
+// settings not allowed by Mimir.
+func validateV0Mimir1HTTPConfig(cfg httpcfg.HTTPClientConfig) error {
+	if cfg.BasicAuth != nil && cfg.BasicAuth.PasswordFile != "" {
+		return errPasswordFileNotAllowed
+	}
+	if cfg.Authorization != nil && cfg.Authorization.CredentialsFile != "" {
+		return errPasswordFileNotAllowed
+	}
+	if cfg.BearerTokenFile != "" {
+		return errPasswordFileNotAllowed
+	}
+	if cfg.OAuth2 != nil {
+		if cfg.OAuth2.ClientSecretFile != "" {
+			return errOAuth2SecretFileNotAllowed
+		}
+		// Mimir's "firewall" doesn't protect OAuth2 client, so we disallow Proxy settings here.
+		if cfg.OAuth2.ProxyURL.URL != nil && cfg.OAuth2.ProxyURL.String() != "" {
+			return errProxyURLNotAllowed
+		}
+		if cfg.OAuth2.ProxyFromEnvironment {
+			return errProxyFromEnvironmentURLNotAllowed
+		}
+	}
+	// We allow setting proxy config (cfg.ProxyConfig), because Mimir's "firewall" protects those calls.
+	return validateV0Mimir1TLSConfig(cfg.TLSConfig)
+}
+
+// validateV0Mimir1TLSConfig validates the v0mimir1 TLS config and returns an error if it contains
+// settings not allowed by Mimir.
+func validateV0Mimir1TLSConfig(cfg httpcfg.TLSConfig) error {
+	if cfg.CAFile != "" || cfg.CertFile != "" || cfg.KeyFile != "" || cfg.CA != "" || cfg.Cert != "" || cfg.Key != "" {
+		return errTLSConfigNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1DiscordConfig validates the v0mimir1 Discord config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1DiscordConfig(cfg discord_v0mimir1.Config) error {
+	if cfg.WebhookURLFile != "" {
+		return errWebhookURLFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1EmailConfig validates the v0mimir1 Email config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1EmailConfig(cfg email_v0mimir1.Config) error {
+	if cfg.AuthPasswordFile != "" {
+		return errPasswordFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1SlackConfig validates the v0mimir1 Slack config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1SlackConfig(cfg slack_v0mimir1.Config) error {
+	if cfg.APIURLFile != "" {
+		return errSlackAPIURLFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1OpsGenieConfig validates the v0mimir1 OpsGenie config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1OpsGenieConfig(cfg opsgenie_v0mimir1.Config) error {
+	if cfg.APIKeyFile != "" {
+		return errOpsGenieAPIKeyFileFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1VictorOpsConfig validates the v0mimir1 VictorOps config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1VictorOpsConfig(cfg victorops_v0mimir1.Config) error {
+	if cfg.APIKeyFile != "" {
+		return errVictorOpsAPIKeyFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1PagerDutyConfig validates the v0mimir1 PagerDuty config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1PagerDutyConfig(cfg pagerduty_v0mimir1.Config) error {
+	if cfg.ServiceKeyFile != "" {
+		return errPagerDutyServiceKeyFileNotAllowed
+	}
+	if cfg.RoutingKeyFile != "" {
+		return errPagerDutyRoutingKeyFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1PushoverConfig validates the v0mimir1 Pushover config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1PushoverConfig(cfg pushover_v0mimir1.Config) error {
+	if cfg.UserKeyFile != "" {
+		return errPushoverUserKeyFileNotAllowed
+	}
+	if cfg.TokenFile != "" {
+		return errPushoverTokenFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1TelegramConfig validates the v0mimir1 Telegram config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1TelegramConfig(cfg telegram_v0mimir1.Config) error {
+	if cfg.BotTokenFile != "" {
+		return errTelegramBotTokenFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1WebhookConfig validates the v0mimir1 Webhook config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1WebhookConfig(cfg webhook_v0mimir1.Config) error {
+	if cfg.URLFile != "" {
+		return errWebhookURLFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir1MSTeamsConfig validates the v0mimir1 MS Teams config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir1MSTeamsConfig(cfg teams_v0mimir1.Config) error {
+	if cfg.WebhookURLFile != "" {
+		return errWebhookURLFileNotAllowed
+	}
+	return nil
+}
+
+// validateV0Mimir2MSTeamsConfig validates the v0mimir2 MS Teams V2 config and returns an error if it
+// contains settings not allowed by Mimir.
+func validateV0Mimir2MSTeamsConfig(cfg teams_v0mimir2.Config) error {
+	if cfg.WebhookURLFile != "" {
+		return errWebhookURLFileNotAllowed
+	}
+	return nil
+}

diff --git a/definition/mimir_validation_test.go b/definition/mimir_validation_test.go
--- a/definition/mimir_validation_test.go
+++ b/definition/mimir_validation_test.go
@@ -3,13 +3,35 @@
 package definition
 
 import (
+	"net/url"
 	"testing"
 
 	"github.com/prometheus/alertmanager/config"
 	commoncfg "github.com/prometheus/common/config"
 	"github.com/stretchr/testify/assert"
+
+	httpcfg "github.com/grafana/alerting/http/v0mimir1"
+	discord_v0mimir1 "github.com/grafana/alerting/receivers/discord/v0mimir1"
+	email_v0mimir1 "github.com/grafana/alerting/receivers/email/v0mimir1"
+	opsgenie_v0mimir1 "github.com/grafana/alerting/receivers/opsgenie/v0mimir1"
+	pagerduty_v0mimir1 "github.com/grafana/alerting/receivers/pagerduty/v0mimir1"
+	pushover_v0mimir1 "github.com/grafana/alerting/receivers/pushover/v0mimir1"
+	slack_v0mimir1 "github.com/grafana/alerting/receivers/slack/v0mimir1"
+	teams_v0mimir1 "github.com/grafana/alerting/receivers/teams/v0mimir1"
+	teams_v0mimir2 "github.com/grafana/alerting/receivers/teams/v0mimir2"
+	telegram_v0mimir1 "github.com/grafana/alerting/receivers/telegram/v0mimir1"
+	victorops_v0mimir1 "github.com/grafana/alerting/receivers/victorops/v0mimir1"
+	webhook_v0mimir1 "github.com/grafana/alerting/receivers/webhook/v0mimir1"
 )
 
+func mustParseURL(s string) *url.URL {
+	u, err := url.Parse(s)
+	if err != nil {
+		panic(err)
+	}
+	return u
+}
+
 func TestValidateAlertmanagerConfig(t *testing.T) {
 	tests := map[string]struct {
 		input    interface{}
@@ -180,6 +202,202 @@
 			},
 			expected: errTLSConfigNotAllowed,
 		},
+
+		// v0mimir1 types tests
+		"v0mimir1 HTTPClientConfig.BasicAuth.PasswordFile": {
+			input: httpcfg.HTTPClientConfig{
+				BasicAuth: &httpcfg.BasicAuth{
+					PasswordFile: "/secrets",
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"v0mimir1 HTTPClientConfig.BearerTokenFile": {
+			input: httpcfg.HTTPClientConfig{
+				BearerTokenFile: "/file",
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"v0mimir1 HTTPClientConfig.Authorization.CredentialsFile": {
+			input: httpcfg.HTTPClientConfig{
+				Authorization: &httpcfg.Authorization{
+					CredentialsFile: "/file",
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"v0mimir1 HTTPClientConfig.OAuth2.ClientSecretFile": {
+			input: httpcfg.HTTPClientConfig{
+				OAuth2: &httpcfg.OAuth2{
+					ClientID:         "client-id",
+					TokenURL:         "http://token-url",
+					ClientSecretFile: "/file",
+				},
+			},
+			expected: errOAuth2SecretFileNotAllowed,
+		},
+		"v0mimir1 HTTPClientConfig.OAuth2.ProxyURL": {
+			input: httpcfg.HTTPClientConfig{
+				OAuth2: &httpcfg.OAuth2{
+					ClientID:     "client-id",
+					TokenURL:     "http://token-url",
+					ClientSecret: "secret",
+					ProxyConfig: httpcfg.ProxyConfig{
+						ProxyURL: commoncfg.URL{URL: mustParseURL("http://proxy:8080")},
+					},
+				},
+			},
+			expected: errProxyURLNotAllowed,
+		},
+		"v0mimir1 HTTPClientConfig.OAuth2.ProxyFromEnvironment": {
+			input: httpcfg.HTTPClientConfig{
+				OAuth2: &httpcfg.OAuth2{
+					ClientID:     "client-id",
+					TokenURL:     "http://token-url",
+					ClientSecret: "secret",
+					ProxyConfig: httpcfg.ProxyConfig{
+						ProxyFromEnvironment: true,
+					},
+				},
+			},
+			expected: errProxyFromEnvironmentURLNotAllowed,
+		},
+		"v0mimir1 TLSConfig.CAFile": {
+			input: httpcfg.TLSConfig{
+				CAFile: "/file",
+			},
+			expected: errTLSConfigNotAllowed,
+		},
+		"v0mimir1 TLSConfig.CertFile": {
+			input: httpcfg.TLSConfig{
+				CertFile: "/file",
+			},
+			expected: errTLSConfigNotAllowed,
+		},
+		"v0mimir1 TLSConfig.KeyFile": {
+			input: httpcfg.TLSConfig{
+				KeyFile: "/file",
+			},
+			expected: errTLSConfigNotAllowed,
+		},
+		"v0mimir1 TLSConfig.CA": {
+			input: httpcfg.TLSConfig{
+				CA: "ca-content",
+			},
+			expected: errTLSConfigNotAllowed,
+		},
+		"v0mimir1 TLSConfig.Cert": {
+			input: httpcfg.TLSConfig{
+				Cert: "cert-content",
+			},
+			expected: errTLSConfigNotAllowed,
+		},
+		"v0mimir1 TLSConfig.Key": {
+			input: httpcfg.TLSConfig{
+				Key: "key-content",
+			},
+			expected: errTLSConfigNotAllowed,
+		},
+		"v0mimir1 DiscordConfig.WebhookURLFile": {
+			input: discord_v0mimir1.Config{
+				WebhookURLFile: "/file",
+			},
+			expected: errWebhookURLFileNotAllowed,
+		},
+		"v0mimir1 EmailConfig.AuthPasswordFile": {
+			input: email_v0mimir1.Config{
+				AuthPasswordFile: "/file",
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"v0mimir1 SlackConfig.APIURLFile": {
+			input: slack_v0mimir1.Config{
+				APIURLFile: "/file",
+			},
+			expected: errSlackAPIURLFileNotAllowed,
+		},
+		"v0mimir1 OpsGenieConfig.APIKeyFile": {
+			input: opsgenie_v0mimir1.Config{
+				APIKeyFile: "/file",
+			},
+			expected: errOpsGenieAPIKeyFileFileNotAllowed,
+		},
+		"v0mimir1 VictorOpsConfig.APIKeyFile": {
+			input: victorops_v0mimir1.Config{
+				APIKeyFile: "/file",
+			},
+			expected: errVictorOpsAPIKeyFileNotAllowed,
+		},
+		"v0mimir1 PagerDutyConfig.ServiceKeyFile": {
+			input: pagerduty_v0mimir1.Config{
+				ServiceKeyFile: "/file",
+			},
+			expected: errPagerDutyServiceKeyFileNotAllowed,
+		},
+		"v0mimir1 PagerDutyConfig.RoutingKeyFile": {
+			input: pagerduty_v0mimir1.Config{
+				RoutingKeyFile: "/file",
+			},
+			expected: errPagerDutyRoutingKeyFileNotAllowed,
+		},
+		"v0mimir1 PushoverConfig.UserKeyFile": {
+			input: pushover_v0mimir1.Config{
+				UserKeyFile: "/file",
+			},
+			expected: errPushoverUserKeyFileNotAllowed,
+		},
+		"v0mimir1 PushoverConfig.TokenFile": {
+			input: pushover_v0mimir1.Config{
+				TokenFile: "/file",
+			},
+			expected: errPushoverTokenFileNotAllowed,
+		},
+		"v0mimir1 TelegramConfig.BotTokenFile": {
+			input: telegram_v0mimir1.Config{
+				BotTokenFile: "/file",
+			},
+			expected: errTelegramBotTokenFileNotAllowed,
+		},
+		"v0mimir1 WebhookConfig.URLFile": {
+			input: webhook_v0mimir1.Config{
+				URLFile: "/file",
+			},
+			expected: errWebhookURLFileNotAllowed,
+		},
+		"v0mimir1 MSTeamsConfig.WebhookURLFile": {
+			input: teams_v0mimir1.Config{
+				WebhookURLFile: "/file",
+			},
+			expected: errWebhookURLFileNotAllowed,
+		},
+		"v0mimir2 MSTeamsV2Config.WebhookURLFile": {
+			input: teams_v0mimir2.Config{
+				WebhookURLFile: "/file",
+			},
+			expected: errWebhookURLFileNotAllowed,
+		},
+		"Receiver with v0mimir1 DiscordConfig.WebhookURLFile": {
+			input: Receiver{
+				Name: "test",
+				DiscordConfigs: []*discord_v0mimir1.Config{{
+					WebhookURLFile: "/file",
+				}},
+			},
+			expected: errWebhookURLFileNotAllowed,
+		},
+		"Receiver with v0mimir1 HTTPClientConfig": {
+			input: Receiver{
+				Name: "test",
+				WebhookConfigs: []*webhook_v0mimir1.Config{{
+					HTTPConfig: &httpcfg.HTTPClientConfig{
+						BasicAuth: &httpcfg.BasicAuth{
+							PasswordFile: "/secrets",
+						},
+					},
+				}},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
 	}
 
 	for testName, testData := range tests {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

1 participant