mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Fix issues with Slack contact points (#40953)
* recipient validation regex modified, validation at creation/modification implemented * Remove validation for recipient, fix tests * Log level changed from Warn to Error
This commit is contained in:
parent
7c5de96503
commit
c9654c4bc0
@ -13,7 +13,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ func init() {
|
|||||||
Label: "Recipient",
|
Label: "Recipient",
|
||||||
Element: alerting.ElementTypeInput,
|
Element: alerting.ElementTypeInput,
|
||||||
InputType: alerting.InputTypeText,
|
InputType: alerting.InputTypeText,
|
||||||
Description: "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace), or user/channel Slack ID - required unless you provide a webhook",
|
Description: "Specify channel, private group, or IM channel (can be an encoded ID or a name) - required unless you provide a webhook",
|
||||||
PropertyName: "recipient",
|
PropertyName: "recipient",
|
||||||
},
|
},
|
||||||
// Logically, this field should be required when not using a webhook, since the Slack API needs a token.
|
// Logically, this field should be required when not using a webhook, since the Slack API needs a token.
|
||||||
@ -119,8 +118,6 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var reRecipient *regexp.Regexp = regexp.MustCompile("^((@[a-z0-9][a-zA-Z0-9._-]*)|(#[^ .A-Z]{1,79})|([a-zA-Z0-9]+))$")
|
|
||||||
|
|
||||||
const slackAPIEndpoint = "https://slack.com/api/chat.postMessage"
|
const slackAPIEndpoint = "https://slack.com/api/chat.postMessage"
|
||||||
|
|
||||||
// NewSlackNotifier is the constructor for the Slack notifier.
|
// NewSlackNotifier is the constructor for the Slack notifier.
|
||||||
@ -135,11 +132,7 @@ func NewSlackNotifier(model *models.AlertNotification, fn alerting.GetDecryptedV
|
|||||||
}
|
}
|
||||||
|
|
||||||
recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString())
|
recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString())
|
||||||
if recipient != "" {
|
if recipient == "" && apiURL.String() == slackAPIEndpoint {
|
||||||
if !reRecipient.MatchString(recipient) {
|
|
||||||
return nil, alerting.ValidationError{Reason: fmt.Sprintf("recipient on invalid format: %q", recipient)}
|
|
||||||
}
|
|
||||||
} else if apiURL.String() == slackAPIEndpoint {
|
|
||||||
return nil, alerting.ValidationError{
|
return nil, alerting.ValidationError{
|
||||||
Reason: "recipient must be specified when using the Slack chat API",
|
Reason: "recipient must be specified when using the Slack chat API",
|
||||||
}
|
}
|
||||||
@ -383,19 +376,21 @@ func (sn *SlackNotifier) sendRequest(ctx context.Context, data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
// Slack responds to some requests with a JSON document, that might contain an error
|
// Slack responds to some requests with a JSON document, that might contain an error.
|
||||||
rslt := struct {
|
rslt := struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
Err string `json:"error"`
|
Err string `json:"error"`
|
||||||
}{}
|
}{}
|
||||||
if err := json.Unmarshal(body, &rslt); err != nil {
|
|
||||||
sn.log.Warn("Failed to unmarshal Slack API response", "url", sn.url.String(), "statusCode", resp.Status,
|
// Marshaling can fail if Slack's response body is plain text (e.g. "ok").
|
||||||
|
if err := json.Unmarshal(body, &rslt); err != nil && json.Valid(body) {
|
||||||
|
sn.log.Error("Failed to unmarshal Slack API response", "url", sn.url.String(), "statusCode", resp.Status,
|
||||||
"err", err)
|
"err", err)
|
||||||
return fmt.Errorf("failed to unmarshal Slack API response with status code %d: %s", resp.StatusCode, err)
|
return fmt.Errorf("failed to unmarshal Slack API response with status code %d: %s", resp.StatusCode, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rslt.Ok && rslt.Err != "" {
|
if !rslt.Ok && rslt.Err != "" {
|
||||||
sn.log.Warn("Sending Slack API request failed", "url", sn.url.String(), "statusCode", resp.Status,
|
sn.log.Error("Sending Slack API request failed", "url", sn.url.String(), "statusCode", resp.Status,
|
||||||
"err", rslt.Err)
|
"err", rslt.Err)
|
||||||
return fmt.Errorf("failed to make Slack API request: %s", rslt.Err)
|
return fmt.Errorf("failed to make Slack API request: %s", rslt.Err)
|
||||||
}
|
}
|
||||||
@ -405,7 +400,7 @@ func (sn *SlackNotifier) sendRequest(ctx context.Context, data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sn.log.Warn("Slack API request failed", "url", sn.url.String(), "statusCode", resp.Status, "body", string(body))
|
sn.log.Error("Slack API request failed", "url", sn.url.String(), "statusCode", resp.Status, "body", string(body))
|
||||||
return fmt.Errorf("request to Slack API failed with status code %d", resp.StatusCode)
|
return fmt.Errorf("request to Slack API failed with status code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,63 +147,6 @@ func TestSlackNotifier(t *testing.T) {
|
|||||||
assert.Equal(t, "xenc-XXXXXXXX-XXXXXXXX-XXXXXXXXXX", slackNotifier.token)
|
assert.Equal(t, "xenc-XXXXXXXX-XXXXXXXX-XXXXXXXXXX", slackNotifier.token)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with channel recipient with spaces should return an error", func(t *testing.T) {
|
|
||||||
json := `
|
|
||||||
{
|
|
||||||
"url": "http://google.com",
|
|
||||||
"recipient": "#open tsdb"
|
|
||||||
}`
|
|
||||||
|
|
||||||
settingsJSON, err := simplejson.NewJson([]byte(json))
|
|
||||||
require.NoError(t, err)
|
|
||||||
model := &models.AlertNotification{
|
|
||||||
Name: "ops",
|
|
||||||
Type: "slack",
|
|
||||||
Settings: settingsJSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
|
|
||||||
assert.EqualError(t, err, "alert validation error: recipient on invalid format: \"#open tsdb\"")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with user recipient with spaces should return an error", func(t *testing.T) {
|
|
||||||
json := `
|
|
||||||
{
|
|
||||||
"url": "http://google.com",
|
|
||||||
"recipient": "@user name"
|
|
||||||
}`
|
|
||||||
|
|
||||||
settingsJSON, err := simplejson.NewJson([]byte(json))
|
|
||||||
require.NoError(t, err)
|
|
||||||
model := &models.AlertNotification{
|
|
||||||
Name: "ops",
|
|
||||||
Type: "slack",
|
|
||||||
Settings: settingsJSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
|
|
||||||
assert.EqualError(t, err, "alert validation error: recipient on invalid format: \"@user name\"")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with user recipient with uppercase letters should return an error", func(t *testing.T) {
|
|
||||||
json := `
|
|
||||||
{
|
|
||||||
"url": "http://google.com",
|
|
||||||
"recipient": "@User"
|
|
||||||
}`
|
|
||||||
|
|
||||||
settingsJSON, err := simplejson.NewJson([]byte(json))
|
|
||||||
require.NoError(t, err)
|
|
||||||
model := &models.AlertNotification{
|
|
||||||
Name: "ops",
|
|
||||||
Type: "slack",
|
|
||||||
Settings: settingsJSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
|
|
||||||
assert.EqualError(t, err, "alert validation error: recipient on invalid format: \"@User\"")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with Slack ID for recipient should work", func(t *testing.T) {
|
t.Run("with Slack ID for recipient should work", func(t *testing.T) {
|
||||||
json := `
|
json := `
|
||||||
{
|
{
|
||||||
@ -273,9 +216,8 @@ func TestSendSlackRequest(t *testing.T) {
|
|||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No response body",
|
name: "No response body",
|
||||||
statusCode: http.StatusOK,
|
statusCode: http.StatusOK,
|
||||||
expectError: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Success case, unexpected response body",
|
name: "Success case, unexpected response body",
|
||||||
|
@ -380,7 +380,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
|||||||
Label: "Recipient",
|
Label: "Recipient",
|
||||||
Element: alerting.ElementTypeInput,
|
Element: alerting.ElementTypeInput,
|
||||||
InputType: alerting.InputTypeText,
|
InputType: alerting.InputTypeText,
|
||||||
Description: "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace), or user/channel Slack ID - required unless you provide a webhook",
|
Description: "Specify channel, private group, or IM channel (can be an encoded ID or a name) - required unless you provide a webhook",
|
||||||
PropertyName: "recipient",
|
PropertyName: "recipient",
|
||||||
},
|
},
|
||||||
// Logically, this field should be required when not using a webhook, since the Slack API needs a token.
|
// Logically, this field should be required when not using a webhook, since the Slack API needs a token.
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -42,8 +41,6 @@ type SlackNotifier struct {
|
|||||||
Token string
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
var reRecipient *regexp.Regexp = regexp.MustCompile("^((@[a-z0-9][a-zA-Z0-9._-]*)|(#[^ .A-Z]{1,79})|([a-zA-Z0-9]+))$")
|
|
||||||
|
|
||||||
var SlackAPIEndpoint = "https://slack.com/api/chat.postMessage"
|
var SlackAPIEndpoint = "https://slack.com/api/chat.postMessage"
|
||||||
|
|
||||||
// NewSlackNotifier is the constructor for the Slack notifier
|
// NewSlackNotifier is the constructor for the Slack notifier
|
||||||
@ -62,11 +59,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn
|
|||||||
}
|
}
|
||||||
|
|
||||||
recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString())
|
recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString())
|
||||||
if recipient != "" {
|
if recipient == "" && apiURL.String() == SlackAPIEndpoint {
|
||||||
if !reRecipient.MatchString(recipient) {
|
|
||||||
return nil, receiverInitError{Cfg: *model, Reason: fmt.Sprintf("recipient on invalid format: %q", recipient)}
|
|
||||||
}
|
|
||||||
} else if apiURL.String() == SlackAPIEndpoint {
|
|
||||||
return nil, receiverInitError{Cfg: *model,
|
return nil, receiverInitError{Cfg: *model,
|
||||||
Reason: "recipient must be specified when using the Slack chat API",
|
Reason: "recipient must be specified when using the Slack chat API",
|
||||||
}
|
}
|
||||||
@ -219,23 +212,25 @@ var sendSlackRequest = func(request *http.Request, logger log.Logger) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
logger.Warn("Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status, "body", string(body))
|
logger.Error("Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status, "body", string(body))
|
||||||
return fmt.Errorf("request to Slack API failed with status code %d", resp.StatusCode)
|
return fmt.Errorf("request to Slack API failed with status code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slack responds to some requests with a JSON document, that might contain an error
|
// Slack responds to some requests with a JSON document, that might contain an error.
|
||||||
rslt := struct {
|
rslt := struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
Err string `json:"error"`
|
Err string `json:"error"`
|
||||||
}{}
|
}{}
|
||||||
if err := json.Unmarshal(body, &rslt); err != nil {
|
|
||||||
logger.Warn("Failed to unmarshal Slack API response", "url", request.URL.String(), "statusCode", resp.Status,
|
// Marshaling can fail if Slack's response body is plain text (e.g. "ok").
|
||||||
|
if err := json.Unmarshal(body, &rslt); err != nil && json.Valid(body) {
|
||||||
|
logger.Error("Failed to unmarshal Slack API response", "url", request.URL.String(), "statusCode", resp.Status,
|
||||||
"body", string(body))
|
"body", string(body))
|
||||||
return fmt.Errorf("failed to unmarshal Slack API response: %s", err)
|
return fmt.Errorf("failed to unmarshal Slack API response: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rslt.Ok && rslt.Err != "" {
|
if !rslt.Ok && rslt.Err != "" {
|
||||||
logger.Warn("Sending Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status,
|
logger.Error("Sending Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status,
|
||||||
"err", rslt.Err)
|
"err", rslt.Err)
|
||||||
return fmt.Errorf("failed to make Slack API request: %s", rslt.Err)
|
return fmt.Errorf("failed to make Slack API request: %s", rslt.Err)
|
||||||
}
|
}
|
||||||
|
@ -270,9 +270,8 @@ func TestSendSlackRequest(t *testing.T) {
|
|||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No response body",
|
name: "No response body",
|
||||||
statusCode: http.StatusOK,
|
statusCode: http.StatusOK,
|
||||||
expectError: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Success case, unexpected response body",
|
name: "Success case, unexpected response body",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package ualert
|
package ualert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -31,17 +30,16 @@ func Test_validateAlertmanagerConfig(t *testing.T) {
|
|||||||
err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q: parse %q: net/url: invalid control character in URL", invalidUri, invalidUri),
|
err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q: parse %q: net/url: invalid control character in URL", invalidUri, invalidUri),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "when a slack receiver has an invalid recipient - it should error",
|
name: "when a slack receiver has an invalid recipient - it should not error",
|
||||||
receivers: []*PostableGrafanaReceiver{
|
receivers: []*PostableGrafanaReceiver{
|
||||||
{
|
{
|
||||||
UID: util.GenerateShortUID(),
|
UID: util.GenerateShortUID(),
|
||||||
Name: "SlackWithBadRecipient",
|
Name: "SlackWithBadRecipient",
|
||||||
Type: "slack",
|
Type: "slack",
|
||||||
Settings: simplejson.NewFromAny(map[string]interface{}{"recipient": "this-doesnt-pass"}),
|
Settings: simplejson.NewFromAny(map[string]interface{}{"recipient": "this passes"}),
|
||||||
SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"},
|
SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
err: errors.New("failed to validate receiver \"SlackWithBadRecipient\" of type \"slack\": recipient on invalid format: \"this-doesnt-pass\""),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "when the configuration is valid - it should not error",
|
name: "when the configuration is valid - it should not error",
|
||||||
|
@ -790,7 +790,7 @@ var expAvailableChannelJsonOutput = `
|
|||||||
"element": "input",
|
"element": "input",
|
||||||
"inputType": "text",
|
"inputType": "text",
|
||||||
"label": "Recipient",
|
"label": "Recipient",
|
||||||
"description": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace), or user/channel Slack ID - required unless you provide a webhook",
|
"description": "Specify channel, private group, or IM channel (can be an encoded ID or a name) - required unless you provide a webhook",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"propertyName": "recipient",
|
"propertyName": "recipient",
|
||||||
"selectOptions": null,
|
"selectOptions": null,
|
||||||
|
Loading…
Reference in New Issue
Block a user