From 79c998c760b6a3d57cd71b55f5eb81aaf6cc2d10 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Fri, 22 Jul 2022 09:41:49 +0100 Subject: [PATCH] Alerting: Allow the webhook notifier to support a custom Authorization header (#52515) (#52621) * Allow the webhook notifier to support a custom Authorization header Instead of doing something clever of re-using the existing username/password fields of Basic Authentication - I opted for two diffent fields to match the upstream Alertmanager configuration (that in turn is based of the HTTP Basic authentication). It'll fail if you have values for both HTTP Basic Authentication and Authorization. (cherry picked from commit b026f2bc5d25a05465bc7cb43875b5231d095f97) --- .../ngalert/notifier/available_channels.go | 34 +++++-- .../ngalert/notifier/channels/webhook.go | 67 ++++++++++---- .../ngalert/notifier/channels/webhook_test.go | 88 +++++++++++++++++-- 3 files changed, 157 insertions(+), 32 deletions(-) diff --git a/pkg/services/ngalert/notifier/available_channels.go b/pkg/services/ngalert/notifier/available_channels.go index 317b59db4bb..022ca53e1f0 100644 --- a/pkg/services/ngalert/notifier/available_channels.go +++ b/pkg/services/ngalert/notifier/available_channels.go @@ -112,7 +112,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Heading: "DingDing settings", Options: []alerting.NotifierOption{ { - Label: "Url", + Label: "URL", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, Placeholder: "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx", @@ -268,7 +268,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Heading: "VictorOps settings", Options: []alerting.NotifierOption{ { - Label: "Url", + Label: "URL", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, Placeholder: "VictorOps url", @@ -608,14 +608,14 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Heading: "Webhook settings", Options: []alerting.NotifierOption{ { - Label: "Url", + Label: "URL", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, PropertyName: "url", Required: true, }, { - Label: "Http Method", + Label: "HTTP Method", Element: alerting.ElementTypeSelect, SelectOptions: []alerting.SelectOption{ { @@ -630,18 +630,34 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { PropertyName: "httpMethod", }, { - Label: "Username", + Label: "HTTP Basic Authentication - Username", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, PropertyName: "username", }, { - Label: "Password", + Label: "HTTP Basic Authentication - Password", Element: alerting.ElementTypeInput, InputType: alerting.InputTypePassword, PropertyName: "password", Secure: true, }, + { // New in 9.1 + Label: "Authorization Header - Scheme", + Description: "Optionally provide a scheme for the Authorization Request Header. Default is Bearer.", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "authorization_scheme", + Placeholder: "Bearer", + }, + { // New in 9.1 + Label: "Authorization Header - Credentials", + Description: "Credentials for the Authorization Request header. Only one of HTTP Basic Authentication or Authorization Request Header can be set.", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "authorization_credentials", + Secure: true, + }, { // New in 8.0. TODO: How to enforce only numbers? Label: "Max Alerts", Description: "Max alerts to include in a notification. Remaining alerts in the same batch will be ignored above this number. 0 means no limit.", @@ -658,7 +674,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Heading: "WeCom settings", Options: []alerting.NotifierOption{ { - Label: "Url", + Label: "URL", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, Placeholder: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx", @@ -747,7 +763,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Heading: "Google Hangouts Chat settings", Options: []alerting.NotifierOption{ { - Label: "Url", + Label: "URL", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, Placeholder: "Google Hangouts Chat incoming webhook url", @@ -833,7 +849,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Secure: true, }, { - Label: "Alert API Url", + Label: "Alert API URL", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, Placeholder: "https://api.opsgenie.com/v2/alerts", diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go index 892dff3061a..4c9d18e6459 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -4,14 +4,16 @@ import ( "context" "encoding/json" "errors" + "fmt" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/notifications" ) // WebhookNotifier is responsible for sending @@ -19,23 +21,32 @@ import ( type WebhookNotifier struct { *Base URL string - User string - Password string HTTPMethod string MaxAlerts int log log.Logger ns notifications.WebhookSender tmpl *template.Template orgID int64 + + User string + Password string + + AuthorizationScheme string + AuthorizationCredentials string } type WebhookConfig struct { *NotificationChannelConfig URL string - User string - Password string HTTPMethod string MaxAlerts int + + // Authorization Header. + AuthorizationScheme string + AuthorizationCredentials string + // HTTP Basic Authentication. + User string + Password string } func WebHookFactory(fc FactoryConfig) (NotificationChannel, error) { @@ -54,11 +65,23 @@ func NewWebHookConfig(config *NotificationChannelConfig, decryptFunc GetDecrypte if url == "" { return nil, errors.New("could not find url property in settings") } + + user := config.Settings.Get("username").MustString() + password := decryptFunc(context.Background(), config.SecureSettings, "password", config.Settings.Get("password").MustString()) + authorizationScheme := config.Settings.Get("authorization_scheme").MustString("Bearer") + authorizationCredentials := decryptFunc(context.Background(), config.SecureSettings, "authorization_credentials", config.Settings.Get("authorization_credentials").MustString()) + + if user != "" && password != "" && authorizationScheme != "" && authorizationCredentials != "" { + return nil, errors.New("both HTTP Basic Authentication and Authorization Header are set, only 1 is permitted") + } + return &WebhookConfig{ NotificationChannelConfig: config, URL: url, - User: config.Settings.Get("username").MustString(), - Password: decryptFunc(context.Background(), config.SecureSettings, "password", config.Settings.Get("password").MustString()), + User: user, + Password: password, + AuthorizationScheme: authorizationScheme, + AuthorizationCredentials: authorizationCredentials, HTTPMethod: config.Settings.Get("httpMethod").MustString("POST"), MaxAlerts: config.Settings.Get("maxAlerts").MustInt(0), }, nil @@ -75,15 +98,17 @@ func NewWebHookNotifier(config *WebhookConfig, ns notifications.WebhookSender, t DisableResolveMessage: config.DisableResolveMessage, Settings: config.Settings, }), - orgID: config.OrgID, - URL: config.URL, - User: config.User, - Password: config.Password, - HTTPMethod: config.HTTPMethod, - MaxAlerts: config.MaxAlerts, - log: log.New("alerting.notifier.webhook"), - ns: ns, - tmpl: t, + orgID: config.OrgID, + URL: config.URL, + User: config.User, + Password: config.Password, + AuthorizationScheme: config.AuthorizationScheme, + AuthorizationCredentials: config.AuthorizationCredentials, + HTTPMethod: config.HTTPMethod, + MaxAlerts: config.MaxAlerts, + log: log.New("alerting.notifier.webhook"), + ns: ns, + tmpl: t, } } @@ -138,12 +163,18 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool return false, err } + headers := make(map[string]string) + if wn.AuthorizationScheme != "" && wn.AuthorizationCredentials != "" { + headers["Authorization"] = fmt.Sprintf("%s %s", wn.AuthorizationScheme, wn.AuthorizationCredentials) + } + cmd := &models.SendWebhookSync{ Url: wn.URL, User: wn.User, Password: wn.Password, Body: string(body), HttpMethod: wn.HTTPMethod, + HttpHeader: headers, } if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil { diff --git a/pkg/services/ngalert/notifier/channels/webhook_test.go b/pkg/services/ngalert/notifier/channels/webhook_test.go index a7bc50f3993..de0d66f6e94 100644 --- a/pkg/services/ngalert/notifier/channels/webhook_test.go +++ b/pkg/services/ngalert/notifier/channels/webhook_test.go @@ -27,13 +27,15 @@ func TestWebhookNotifier(t *testing.T) { orgID := int64(1) cases := []struct { - name string - settings string - alerts []*types.Alert + name string + settings string + alerts []*types.Alert + expMsg *webhookMessage expUrl string expUsername string expPassword string + expHeaders map[string]string expHttpMethod string expInitError string expMsgError error @@ -91,7 +93,9 @@ func TestWebhookNotifier(t *testing.T) { OrgID: orgID, }, expMsgError: nil, - }, { + expHeaders: map[string]string{}, + }, + { name: "Custom config with multiple alerts", settings: `{ "url": "http://localhost/test1", @@ -169,7 +173,80 @@ func TestWebhookNotifier(t *testing.T) { OrgID: orgID, }, expMsgError: nil, - }, { + expHeaders: map[string]string{}, + }, + { + name: "with Authorization set", + settings: `{ + "url": "http://localhost/test1", + "authorization_credentials": "mysecret", + "httpMethod": "POST", + "maxAlerts": 2 + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: &webhookMessage{ + ExtendedData: &ExtendedData{ + Receiver: "my_receiver", + Status: "firing", + Alerts: ExtendedAlerts{ + { + Status: "firing", + Labels: template.KV{ + "alertname": "alert1", + "lbl1": "val1", + }, + Annotations: template.KV{ + "ann1": "annv1", + }, + Fingerprint: "fac0861a85de433a", + DashboardURL: "http://localhost/d/abcd", + PanelURL: "http://localhost/d/abcd?viewPanel=efgh", + SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1", + }, + }, + GroupLabels: template.KV{ + "alertname": "", + }, + CommonLabels: template.KV{ + "alertname": "alert1", + "lbl1": "val1", + }, + CommonAnnotations: template.KV{ + "ann1": "annv1", + }, + ExternalURL: "http://localhost", + }, + Version: "1", + GroupKey: "alertname", + Title: "[FIRING:1] (val1)", + State: "alerting", + Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + OrgID: orgID, + }, + expUrl: "http://localhost/test1", + expHttpMethod: "POST", + expHeaders: map[string]string{"Authorization": "Bearer mysecret"}, + }, + { + name: "with both HTTP basic auth and Authorization Header set", + settings: `{ + "url": "http://localhost/test1", + "username": "user1", + "password": "mysecret", + "authorization_credentials": "mysecret", + "httpMethod": "POST", + "maxAlerts": 2 + }`, + expInitError: "both HTTP Basic Authentication and Authorization Header are set, only 1 is permitted", + }, + { name: "Error in initing", settings: `{}`, expInitError: `could not find url property in settings`, @@ -223,6 +300,7 @@ func TestWebhookNotifier(t *testing.T) { require.Equal(t, c.expUsername, webhookSender.Webhook.User) require.Equal(t, c.expPassword, webhookSender.Webhook.Password) require.Equal(t, c.expHttpMethod, webhookSender.Webhook.HttpMethod) + require.Equal(t, c.expHeaders, webhookSender.Webhook.HttpHeader) }) } }