From 4b46da9214998b94d6b7810fd671c1361427bcd1 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 18 Jun 2025 17:01:19 +0200 Subject: [PATCH] Alerting: Hide extra Prometheus configuration secrets (#106916) --- .../ngalert/api/api_convert_prometheus.go | 15 ++++- .../api/api_convert_prometheus_test.go | 30 +++++++-- pkg/services/ngalert/api/tooling/api.json | 64 ++++++++++++++++++- .../definitions/convert_prometheus_api.go | 30 +++++++++ pkg/services/ngalert/api/tooling/post.json | 64 +++++++++++++++++-- pkg/services/ngalert/api/tooling/spec.json | 64 +++++++++++++++++-- ...pi_convert_prometheus_alertmanager_test.go | 57 ++++++++--------- pkg/tests/api/alerting/testing.go | 6 +- public/api-merged.json | 64 ++++++++++++++++++- public/openapi3.json | 64 ++++++++++++++++++- 10 files changed, 400 insertions(+), 58 deletions(-) diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index 14cd0f486f4..1d6f09cc578 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -605,9 +605,18 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c * return response.Error(http.StatusNotFound, "Alertmanager configuration not found", nil) } - respBody := apimodels.AlertmanagerUserConfig{ - AlertmanagerConfig: extraCfg.AlertmanagerConfig, - TemplateFiles: extraCfg.TemplateFiles, + // Parse the configuration into our Gettable struct which will automatically + // sanitize secrets and exclude global settings when marshaled back to YAML. + var prometheusConfig amconfig.Config + if err := yaml.Unmarshal([]byte(extraCfg.AlertmanagerConfig), &prometheusConfig); err != nil { + return response.Error(http.StatusBadRequest, "Invalid Alertmanager configuration format", err) + } + + respBody := apimodels.GettableAlertmanagerUserConfig{ + AlertmanagerConfig: apimodels.GettableAlertmanagerConfig{ + Config: prometheusConfig, + }, + TemplateFiles: extraCfg.TemplateFiles, } resp := response.YAML(http.StatusOK, respBody) diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 4be21edc77d..9c363748fbd 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -1622,6 +1622,7 @@ func TestRouteConvertPrometheusGetAlertmanagerConfig(t *testing.T) { ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI) srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft)) + // Create a config with secrets to check that they will be hided in the response. expectedConfig := apimodels.GettableUserConfig{ ExtraConfigs: []apimodels.ExtraConfiguration{ { @@ -1630,9 +1631,14 @@ func TestRouteConvertPrometheusGetAlertmanagerConfig(t *testing.T) { "test.tmpl": "{{ define \"test\" }}Hello{{ end }}", }, AlertmanagerConfig: `route: - receiver: default + receiver: webhook receivers: - - name: default`, + - name: webhook + webhook_configs: + - url: "http://localhost/webhook" + http_config: + bearer_token: "some-token" +`, }, }, } @@ -1645,11 +1651,25 @@ receivers: require.Equal(t, http.StatusOK, response.Status()) - expectedResponse := `alertmanager_config: |- + expectedResponse := `alertmanager_config: route: - receiver: default + receiver: webhook + continue: false receivers: - - name: default + - name: webhook + webhook_configs: + - url: "" + url_file: "" + http_config: + authorization: + type: "Bearer" + credentials: "" + enable_http2: true + follow_redirects: true + send_resolved: true + max_alerts: 0 + timeout: "0s" + templates: [] template_files: test.tmpl: '{{ define "test" }}Hello{{ end }}'` diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index bf2a859fd61..1d7db10dc80 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -742,12 +742,18 @@ "type": "array" }, "mute_time_intervals": { - "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", + "description": "Deprecated. Remove before v1.0 release.", "items": { "$ref": "#/definitions/MuteTimeInterval" }, "type": "array" }, + "receivers": { + "items": { + "$ref": "#/definitions/Receiver" + }, + "type": "array" + }, "route": { "$ref": "#/definitions/Route" }, @@ -1438,6 +1444,62 @@ "title": "Frames is a slice of Frame pointers.", "type": "array" }, + "GettableAlertmanagerConfig": { + "properties": { + "global": { + "$ref": "#/definitions/GlobalConfig" + }, + "inhibit_rules": { + "items": { + "$ref": "#/definitions/InhibitRule" + }, + "type": "array" + }, + "mute_time_intervals": { + "description": "Deprecated. Remove before v1.0 release.", + "items": { + "$ref": "#/definitions/MuteTimeInterval" + }, + "type": "array" + }, + "receivers": { + "items": { + "$ref": "#/definitions/Receiver" + }, + "type": "array" + }, + "route": { + "$ref": "#/definitions/Route" + }, + "templates": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" + } + }, + "type": "object" + }, + "GettableAlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/GettableAlertmanagerConfig" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "GettableAlertmanagers": { "properties": { "data": { diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index 98b17d19ed3..a3fa9645daf 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -1,6 +1,9 @@ package definitions import ( + "encoding/json" + + "github.com/prometheus/alertmanager/config" "github.com/prometheus/common/model" ) @@ -350,3 +353,30 @@ type AlertmanagerUserConfig struct { AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"` TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` } + +// GettableAlertmanagerUserConfig is like AlertmanagerUserConfig but uses the normal config structure +// that automatically sanitizes secrets when marshaled to YAML/JSON. + +// swagger:model +type GettableAlertmanagerUserConfig struct { + AlertmanagerConfig GettableAlertmanagerConfig `yaml:"alertmanager_config" json:"alertmanager_config"` + TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` +} + +type GettableAlertmanagerConfig struct { + config.Config `yaml:",inline" json:",inline"` +} + +func (c GettableAlertmanagerConfig) MarshalYAML() (any, error) { + type base config.Config + cfg := base(c.Config) + cfg.Global = nil // not used in Grafana + return cfg, nil +} + +func (c GettableAlertmanagerConfig) MarshalJSON() ([]byte, error) { + type base config.Config + cfg := base(c.Config) + cfg.Global = nil // not used in Grafana + return json.Marshal(cfg) +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 84e4a4826d5..22a9d97e913 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -1438,6 +1438,62 @@ "title": "Frames is a slice of Frame pointers.", "type": "array" }, + "GettableAlertmanagerConfig": { + "properties": { + "global": { + "$ref": "#/definitions/GlobalConfig" + }, + "inhibit_rules": { + "items": { + "$ref": "#/definitions/InhibitRule" + }, + "type": "array" + }, + "mute_time_intervals": { + "description": "Deprecated. Remove before v1.0 release.", + "items": { + "$ref": "#/definitions/MuteTimeInterval" + }, + "type": "array" + }, + "receivers": { + "items": { + "$ref": "#/definitions/Receiver" + }, + "type": "array" + }, + "route": { + "$ref": "#/definitions/Route" + }, + "templates": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" + } + }, + "type": "object" + }, + "GettableAlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/GettableAlertmanagerConfig" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "GettableAlertmanagers": { "properties": { "data": { @@ -3709,7 +3765,6 @@ "type": "object" }, "Route": { - "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { "active_time_intervals": { "items": { @@ -3751,12 +3806,6 @@ }, "type": "array" }, - "object_matchers": { - "$ref": "#/definitions/ObjectMatchers" - }, - "provenance": { - "$ref": "#/definitions/Provenance" - }, "receiver": { "type": "string" }, @@ -3770,6 +3819,7 @@ "type": "array" } }, + "title": "A Route is a node that contains definitions of how to handle alerts.", "type": "object" }, "RouteExport": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 1b2744b976d..b6c5f065f24 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -5727,6 +5727,62 @@ "$ref": "#/definitions/Frame" } }, + "GettableAlertmanagerConfig": { + "type": "object", + "properties": { + "global": { + "$ref": "#/definitions/GlobalConfig" + }, + "inhibit_rules": { + "type": "array", + "items": { + "$ref": "#/definitions/InhibitRule" + } + }, + "mute_time_intervals": { + "description": "Deprecated. Remove before v1.0 release.", + "type": "array", + "items": { + "$ref": "#/definitions/MuteTimeInterval" + } + }, + "receivers": { + "type": "array", + "items": { + "$ref": "#/definitions/Receiver" + } + }, + "route": { + "$ref": "#/definitions/Route" + }, + "templates": { + "type": "array", + "items": { + "type": "string" + } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } + } + } + }, + "GettableAlertmanagerUserConfig": { + "type": "object", + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/GettableAlertmanagerConfig" + }, + "template_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "GettableAlertmanagers": { "type": "object", "properties": { @@ -7999,8 +8055,8 @@ } }, "Route": { - "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "type": "object", + "title": "A Route is a node that contains definitions of how to handle alerts.", "properties": { "active_time_intervals": { "type": "array", @@ -8042,12 +8098,6 @@ "type": "string" } }, - "object_matchers": { - "$ref": "#/definitions/ObjectMatchers" - }, - "provenance": { - "$ref": "#/definitions/Provenance" - }, "receiver": { "type": "string" }, diff --git a/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go b/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go index acd788e84fc..acc37c31ea8 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go @@ -11,19 +11,15 @@ import ( ) const testAlertmanagerConfigYAML = ` -global: - smtp_smarthost: localhost:587 - smtp_from: alertmanager@example.org - route: group_by: ['alertname'] group_wait: 10s group_interval: 10s repeat_interval: 1h - receiver: web.hook + receiver: webhook receivers: -- name: web.hook +- name: webhook webhook_configs: - url: 'http://127.0.0.1:5001/' @@ -53,8 +49,17 @@ func TestIntegrationConvertPrometheusAlertmanagerEndpoints(t *testing.T) { apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + cleanup := func(identifier string) { + deleteHeaders := map[string]string{ + "X-Grafana-Alerting-Config-Identifier": identifier, + } + _, status, _ := apiClient.RawConvertPrometheusDeleteAlertmanagerConfig(t, deleteHeaders) + require.Equal(t, http.StatusAccepted, status) + } + t.Run("create and get alertmanager configuration", func(t *testing.T) { identifier := "test-create-get-config" + defer cleanup(identifier) mergeMatchers := "environment=production,team=backend" headers := map[string]string{ @@ -81,19 +86,15 @@ func TestIntegrationConvertPrometheusAlertmanagerEndpoints(t *testing.T) { require.Contains(t, retrievedConfig.TemplateFiles, "test.tmpl") require.Equal(t, `{{ define "test.template" }}Test template{{ end }}`, retrievedConfig.TemplateFiles["test.tmpl"]) - // Verify the configuration contains expected content - require.Contains(t, retrievedConfig.AlertmanagerConfig, "smtp_smarthost: localhost:587") - require.Contains(t, retrievedConfig.AlertmanagerConfig, "web.hook") - - // Cleanup - deleteHeaders := map[string]string{ - "X-Grafana-Alerting-Config-Identifier": identifier, - } - apiClient.ConvertPrometheusDeleteAlertmanagerConfig(t, deleteHeaders) + require.Len(t, retrievedConfig.AlertmanagerConfig.Receivers, 1) + require.Equal(t, "webhook", retrievedConfig.AlertmanagerConfig.Receivers[0].Name) + require.Len(t, retrievedConfig.AlertmanagerConfig.Receivers[0].WebhookConfigs, 1) + require.Equal(t, "", retrievedConfig.AlertmanagerConfig.Receivers[0].WebhookConfigs[0].URL.String()) }) t.Run("delete alertmanager configuration", func(t *testing.T) { identifier := "test-delete-config" + defer cleanup(identifier) mergeMatchers := "environment=production,team=backend" headers := map[string]string{ @@ -194,6 +195,7 @@ func TestIntegrationConvertPrometheusAlertmanagerEndpoints(t *testing.T) { t.Run("update existing configuration", func(t *testing.T) { identifier := "test-update-config" + defer cleanup(identifier) headers := map[string]string{ "Content-Type": "application/yaml", @@ -213,19 +215,15 @@ func TestIntegrationConvertPrometheusAlertmanagerEndpoints(t *testing.T) { // Update the same configuration with new content updatedConfigYAML := ` -global: - smtp_smarthost: localhost:25 - smtp_from: updated@example.org - route: group_by: ['service'] group_wait: 5s group_interval: 5s repeat_interval: 30m - receiver: updated.hook + receiver: updated-webhook receivers: -- name: updated.hook +- name: updated-webhook webhook_configs: - url: 'http://127.0.0.1:8080/updated' ` @@ -245,21 +243,20 @@ receivers: "X-Grafana-Alerting-Config-Identifier": identifier, } retrievedConfig := apiClient.ConvertPrometheusGetAlertmanagerConfig(t, getHeaders) - require.NotEmpty(t, retrievedConfig.AlertmanagerConfig) - require.Contains(t, retrievedConfig.AlertmanagerConfig, "updated@example.org") - require.Contains(t, retrievedConfig.AlertmanagerConfig, "updated.hook") - require.Contains(t, retrievedConfig.TemplateFiles, "updated.tmpl") - require.Equal(t, `{{ define "updated.template" }}Updated Config{{ end }}`, retrievedConfig.TemplateFiles["updated.tmpl"]) - deleteHeaders := map[string]string{ - "X-Grafana-Alerting-Config-Identifier": identifier, - } - apiClient.ConvertPrometheusDeleteAlertmanagerConfig(t, deleteHeaders) + require.NotEmpty(t, retrievedConfig.AlertmanagerConfig) + require.Len(t, retrievedConfig.AlertmanagerConfig.Receivers, 1) + require.Equal(t, "updated-webhook", retrievedConfig.AlertmanagerConfig.Receivers[0].Name) + require.Len(t, retrievedConfig.AlertmanagerConfig.Receivers[0].WebhookConfigs, 1) + require.Equal(t, "", retrievedConfig.AlertmanagerConfig.Receivers[0].WebhookConfigs[0].URL.String()) + + require.Equal(t, `{{ define "updated.template" }}Updated Config{{ end }}`, retrievedConfig.TemplateFiles["updated.tmpl"]) }) t.Run("multiple extra configurations conflict", func(t *testing.T) { firstIdentifier := "first-config" secondIdentifier := "second-config" + defer cleanup(firstIdentifier) // Create first configuration firstHeaders := map[string]string{ diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index 654369326c1..c9856265ba2 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -1300,7 +1300,7 @@ func (a apiClient) RawConvertPrometheusPostAlertmanagerConfig(t *testing.T, amCf return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) } -func (a apiClient) ConvertPrometheusGetAlertmanagerConfig(t *testing.T, headers map[string]string) apimodels.AlertmanagerUserConfig { +func (a apiClient) ConvertPrometheusGetAlertmanagerConfig(t *testing.T, headers map[string]string) apimodels.GettableAlertmanagerUserConfig { t.Helper() config, status, raw := a.RawConvertPrometheusGetAlertmanagerConfig(t, headers) @@ -1309,7 +1309,7 @@ func (a apiClient) ConvertPrometheusGetAlertmanagerConfig(t *testing.T, headers return config } -func (a apiClient) RawConvertPrometheusGetAlertmanagerConfig(t *testing.T, headers map[string]string) (apimodels.AlertmanagerUserConfig, int, string) { +func (a apiClient) RawConvertPrometheusGetAlertmanagerConfig(t *testing.T, headers map[string]string) (apimodels.GettableAlertmanagerUserConfig, int, string) { t.Helper() path := "%s/api/convert/api/v1/alerts" @@ -1321,7 +1321,7 @@ func (a apiClient) RawConvertPrometheusGetAlertmanagerConfig(t *testing.T, heade req.Header.Set(key, value) } - config, status, raw := sendRequestYAML[apimodels.AlertmanagerUserConfig](t, req, http.StatusOK) + config, status, raw := sendRequestYAML[apimodels.GettableAlertmanagerUserConfig](t, req, http.StatusOK) return config, status, raw } diff --git a/public/api-merged.json b/public/api-merged.json index c6da3736e5e..0701fd0f9e5 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -13895,12 +13895,18 @@ } }, "mute_time_intervals": { - "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", + "description": "Deprecated. Remove before v1.0 release.", "type": "array", "items": { "$ref": "#/definitions/MuteTimeInterval" } }, + "receivers": { + "type": "array", + "items": { + "$ref": "#/definitions/Receiver" + } + }, "route": { "$ref": "#/definitions/Route" }, @@ -15887,6 +15893,62 @@ } } }, + "GettableAlertmanagerConfig": { + "type": "object", + "properties": { + "global": { + "$ref": "#/definitions/GlobalConfig" + }, + "inhibit_rules": { + "type": "array", + "items": { + "$ref": "#/definitions/InhibitRule" + } + }, + "mute_time_intervals": { + "description": "Deprecated. Remove before v1.0 release.", + "type": "array", + "items": { + "$ref": "#/definitions/MuteTimeInterval" + } + }, + "receivers": { + "type": "array", + "items": { + "$ref": "#/definitions/Receiver" + } + }, + "route": { + "$ref": "#/definitions/Route" + }, + "templates": { + "type": "array", + "items": { + "type": "string" + } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } + } + } + }, + "GettableAlertmanagerUserConfig": { + "type": "object", + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/GettableAlertmanagerConfig" + }, + "template_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "GettableAlertmanagers": { "type": "object", "properties": { diff --git a/public/openapi3.json b/public/openapi3.json index 2ad1082bae6..ad5d30a66be 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -3944,12 +3944,18 @@ "type": "array" }, "mute_time_intervals": { - "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", + "description": "Deprecated. Remove before v1.0 release.", "items": { "$ref": "#/components/schemas/MuteTimeInterval" }, "type": "array" }, + "receivers": { + "items": { + "$ref": "#/components/schemas/Receiver" + }, + "type": "array" + }, "route": { "$ref": "#/components/schemas/Route" }, @@ -5938,6 +5944,62 @@ }, "type": "object" }, + "GettableAlertmanagerConfig": { + "properties": { + "global": { + "$ref": "#/components/schemas/GlobalConfig" + }, + "inhibit_rules": { + "items": { + "$ref": "#/components/schemas/InhibitRule" + }, + "type": "array" + }, + "mute_time_intervals": { + "description": "Deprecated. Remove before v1.0 release.", + "items": { + "$ref": "#/components/schemas/MuteTimeInterval" + }, + "type": "array" + }, + "receivers": { + "items": { + "$ref": "#/components/schemas/Receiver" + }, + "type": "array" + }, + "route": { + "$ref": "#/components/schemas/Route" + }, + "templates": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/components/schemas/TimeInterval" + }, + "type": "array" + } + }, + "type": "object" + }, + "GettableAlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "$ref": "#/components/schemas/GettableAlertmanagerConfig" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "GettableAlertmanagers": { "properties": { "data": {