Alerting: Allow the webhook notifier to support a custom Authorization header (#52515)

* 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.
This commit is contained in:
gotjosh 2022-07-21 10:25:58 +01:00 committed by GitHub
parent b1f355fddc
commit b026f2bc5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 159 additions and 34 deletions

View File

@ -4,15 +4,17 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "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/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications" "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 // WebhookNotifier is responsible for sending
@ -20,8 +22,6 @@ import (
type WebhookNotifier struct { type WebhookNotifier struct {
*Base *Base
URL string URL string
User string
Password string
HTTPMethod string HTTPMethod string
MaxAlerts int MaxAlerts int
log log.Logger log log.Logger
@ -29,15 +29,26 @@ type WebhookNotifier struct {
images ImageStore images ImageStore
tmpl *template.Template tmpl *template.Template
orgID int64 orgID int64
User string
Password string
AuthorizationScheme string
AuthorizationCredentials string
} }
type WebhookConfig struct { type WebhookConfig struct {
*NotificationChannelConfig *NotificationChannelConfig
URL string URL string
User string
Password string
HTTPMethod string HTTPMethod string
MaxAlerts int MaxAlerts int
// Authorization Header.
AuthorizationScheme string
AuthorizationCredentials string
// HTTP Basic Authentication.
User string
Password string
} }
func WebHookFactory(fc FactoryConfig) (NotificationChannel, error) { func WebHookFactory(fc FactoryConfig) (NotificationChannel, error) {
@ -56,11 +67,23 @@ func NewWebHookConfig(config *NotificationChannelConfig, decryptFunc GetDecrypte
if url == "" { if url == "" {
return nil, errors.New("could not find url property in settings") 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{ return &WebhookConfig{
NotificationChannelConfig: config, NotificationChannelConfig: config,
URL: url, URL: url,
User: config.Settings.Get("username").MustString(), User: user,
Password: decryptFunc(context.Background(), config.SecureSettings, "password", config.Settings.Get("password").MustString()), Password: password,
AuthorizationScheme: authorizationScheme,
AuthorizationCredentials: authorizationCredentials,
HTTPMethod: config.Settings.Get("httpMethod").MustString("POST"), HTTPMethod: config.Settings.Get("httpMethod").MustString("POST"),
MaxAlerts: config.Settings.Get("maxAlerts").MustInt(0), MaxAlerts: config.Settings.Get("maxAlerts").MustInt(0),
}, nil }, nil
@ -77,16 +100,18 @@ func NewWebHookNotifier(config *WebhookConfig, ns notifications.WebhookSender, i
DisableResolveMessage: config.DisableResolveMessage, DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings, Settings: config.Settings,
}), }),
orgID: config.OrgID, orgID: config.OrgID,
URL: config.URL, URL: config.URL,
User: config.User, User: config.User,
Password: config.Password, Password: config.Password,
HTTPMethod: config.HTTPMethod, AuthorizationScheme: config.AuthorizationScheme,
MaxAlerts: config.MaxAlerts, AuthorizationCredentials: config.AuthorizationCredentials,
log: log.New("alerting.notifier.webhook"), HTTPMethod: config.HTTPMethod,
ns: ns, MaxAlerts: config.MaxAlerts,
images: images, log: log.New("alerting.notifier.webhook"),
tmpl: t, ns: ns,
images: images,
tmpl: t,
} }
} }
@ -152,12 +177,18 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
return false, err 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{ cmd := &models.SendWebhookSync{
Url: wn.URL, Url: wn.URL,
User: wn.User, User: wn.User,
Password: wn.Password, Password: wn.Password,
Body: string(body), Body: string(body),
HttpMethod: wn.HTTPMethod, HttpMethod: wn.HTTPMethod,
HttpHeader: headers,
} }
if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil { if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil {

View File

@ -27,13 +27,15 @@ func TestWebhookNotifier(t *testing.T) {
orgID := int64(1) orgID := int64(1)
cases := []struct { cases := []struct {
name string name string
settings string settings string
alerts []*types.Alert alerts []*types.Alert
expMsg *webhookMessage expMsg *webhookMessage
expUrl string expUrl string
expUsername string expUsername string
expPassword string expPassword string
expHeaders map[string]string
expHttpMethod string expHttpMethod string
expInitError string expInitError string
expMsgError error expMsgError error
@ -91,7 +93,9 @@ func TestWebhookNotifier(t *testing.T) {
OrgID: orgID, OrgID: orgID,
}, },
expMsgError: nil, expMsgError: nil,
}, { expHeaders: map[string]string{},
},
{
name: "Custom config with multiple alerts", name: "Custom config with multiple alerts",
settings: `{ settings: `{
"url": "http://localhost/test1", "url": "http://localhost/test1",
@ -169,7 +173,80 @@ func TestWebhookNotifier(t *testing.T) {
OrgID: orgID, OrgID: orgID,
}, },
expMsgError: nil, 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", name: "Error in initing",
settings: `{}`, settings: `{}`,
expInitError: `could not find url property in 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.expUsername, webhookSender.Webhook.User)
require.Equal(t, c.expPassword, webhookSender.Webhook.Password) require.Equal(t, c.expPassword, webhookSender.Webhook.Password)
require.Equal(t, c.expHttpMethod, webhookSender.Webhook.HttpMethod) require.Equal(t, c.expHttpMethod, webhookSender.Webhook.HttpMethod)
require.Equal(t, c.expHeaders, webhookSender.Webhook.HttpHeader)
}) })
} }
} }

View File

@ -112,7 +112,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "DingDing settings", Heading: "DingDing settings",
Options: []alerting.NotifierOption{ Options: []alerting.NotifierOption{
{ {
Label: "Url", Label: "URL",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText, InputType: alerting.InputTypeText,
Placeholder: "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx", Placeholder: "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx",
@ -276,7 +276,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "VictorOps settings", Heading: "VictorOps settings",
Options: []alerting.NotifierOption{ Options: []alerting.NotifierOption{
{ {
Label: "Url", Label: "URL",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText, InputType: alerting.InputTypeText,
Placeholder: "VictorOps url", Placeholder: "VictorOps url",
@ -631,14 +631,14 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "Webhook settings", Heading: "Webhook settings",
Options: []alerting.NotifierOption{ Options: []alerting.NotifierOption{
{ {
Label: "Url", Label: "URL",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText, InputType: alerting.InputTypeText,
PropertyName: "url", PropertyName: "url",
Required: true, Required: true,
}, },
{ {
Label: "Http Method", Label: "HTTP Method",
Element: alerting.ElementTypeSelect, Element: alerting.ElementTypeSelect,
SelectOptions: []alerting.SelectOption{ SelectOptions: []alerting.SelectOption{
{ {
@ -653,18 +653,34 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
PropertyName: "httpMethod", PropertyName: "httpMethod",
}, },
{ {
Label: "Username", Label: "HTTP Basic Authentication - Username",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText, InputType: alerting.InputTypeText,
PropertyName: "username", PropertyName: "username",
}, },
{ {
Label: "Password", Label: "HTTP Basic Authentication - Password",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypePassword, InputType: alerting.InputTypePassword,
PropertyName: "password", PropertyName: "password",
Secure: true, 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? { // New in 8.0. TODO: How to enforce only numbers?
Label: "Max Alerts", 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.", 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", Heading: "WeCom settings",
Options: []alerting.NotifierOption{ Options: []alerting.NotifierOption{
{ {
Label: "Url", Label: "URL",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText, InputType: alerting.InputTypeText,
Placeholder: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx", 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", Heading: "Google Hangouts Chat settings",
Options: []alerting.NotifierOption{ Options: []alerting.NotifierOption{
{ {
Label: "Url", Label: "URL",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText, InputType: alerting.InputTypeText,
Placeholder: "Google Hangouts Chat incoming webhook url", Placeholder: "Google Hangouts Chat incoming webhook url",
@ -864,7 +880,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Secure: true, Secure: true,
}, },
{ {
Label: "Alert API Url", Label: "Alert API URL",
Element: alerting.ElementTypeInput, Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText, InputType: alerting.InputTypeText,
Placeholder: "https://api.opsgenie.com/v2/alerts", Placeholder: "https://api.opsgenie.com/v2/alerts",