diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go index 4e92444cfed..326a6c4b78e 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -4,15 +4,17 @@ import ( "context" "encoding/json" "errors" + "fmt" + + "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" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/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" ) // WebhookNotifier is responsible for sending @@ -20,8 +22,6 @@ import ( type WebhookNotifier struct { *Base URL string - User string - Password string HTTPMethod string MaxAlerts int log log.Logger @@ -29,15 +29,26 @@ type WebhookNotifier struct { images ImageStore 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) { @@ -56,11 +67,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 @@ -77,16 +100,18 @@ func NewWebHookNotifier(config *WebhookConfig, ns notifications.WebhookSender, i 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, - images: images, - 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, + images: images, + tmpl: t, } } @@ -152,12 +177,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 e8b594b8fd7..5dd1798e8ed 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) }) } } diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index f3b75e27757..f4c2f6bb89d 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/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", @@ -276,7 +276,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Heading: "VictorOps settings", Options: []alerting.NotifierOption{ { - Label: "Url", + Label: "URL", Element: alerting.ElementTypeInput, InputType: alerting.InputTypeText, Placeholder: "VictorOps url", @@ -631,14 +631,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{ { @@ -653,18 +653,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.", @@ -681,7 +697,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", @@ -778,7 +794,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", @@ -864,7 +880,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",