Merge pull request #1823 from mxinden/cut-v0.16.2

*: Cut v0.16.2
This commit is contained in:
Max Inden 2019-04-05 14:25:19 +02:00 committed by GitHub
commit 308b762064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 286 additions and 27 deletions

View File

@ -1,3 +1,12 @@
## 0.16.2 / 2019-04-03
Updating to v0.16.2 is recommended for all users using the Slack, Pagerduty,
Hipchat, Wechat, VictorOps and Pushover notifier, as connection errors could
leak secrets embedded in the notifier's URL to stdout.
* [BUGFIX] Redact notifier URL from logs to not leak secrets embedded in the URL (#1822, #1825)
* [BUGFIX] Allow sending of unauthenticated SMTP requests when `smtp_auth_username` is not supplied (#1739)
## 0.16.1 / 2019-01-31 ## 0.16.1 / 2019-01-31
* [BUGFIX] Do not populate cluster info if clustering is disabled in API v2 (#1726) * [BUGFIX] Do not populate cluster info if clustering is disabled in API v2 (#1726)

View File

@ -28,7 +28,7 @@ PRECHECK_OPTIONS_bzr = version
build-all: assets apiv2 build build-all: assets apiv2 build
assets: ui/app/script.js ui/app/index.html ui/app/lib template/default.tmpl assets: ui/app/script.js ui/app/index.html ui/app/lib template/default.tmpl
cd $(PREFIX)/asset && $(GO) generate GO111MODULE=$(GO111MODULE) $(GO) generate ./asset
@$(GOFMT) -w ./asset @$(GOFMT) -w ./asset
ui/app/script.js: $(shell find ui/app/src -iname *.elm) api/v2/openapi.yaml ui/app/script.js: $(shell find ui/app/src -iname *.elm) api/v2/openapi.yaml

View File

@ -1 +1 @@
0.16.1 0.16.2

View File

@ -221,6 +221,13 @@ func NewEmail(c *config.EmailConfig, t *template.Template, l log.Logger) *Email
// auth resolves a string of authentication mechanisms. // auth resolves a string of authentication mechanisms.
func (n *Email) auth(mechs string) (smtp.Auth, error) { func (n *Email) auth(mechs string) (smtp.Auth, error) {
username := n.conf.AuthUsername username := n.conf.AuthUsername
// If no username is set, keep going without authentication.
if n.conf.AuthUsername == "" {
level.Debug(n.logger).Log("msg", "smtp_auth_username is not configured. Attempting to send email without authenticating")
return nil, nil
}
err := &types.MultiError{} err := &types.MultiError{}
for _, mech := range strings.Split(mechs, " ") { for _, mech := range strings.Split(mechs, " ") {
switch mech { switch mech {
@ -464,11 +471,16 @@ type PagerDuty struct {
conf *config.PagerdutyConfig conf *config.PagerdutyConfig
tmpl *template.Template tmpl *template.Template
logger log.Logger logger log.Logger
apiV1 string // for tests.
} }
// NewPagerDuty returns a new PagerDuty notifier. // NewPagerDuty returns a new PagerDuty notifier.
func NewPagerDuty(c *config.PagerdutyConfig, t *template.Template, l log.Logger) *PagerDuty { func NewPagerDuty(c *config.PagerdutyConfig, t *template.Template, l log.Logger) *PagerDuty {
return &PagerDuty{conf: c, tmpl: t, logger: l} n := &PagerDuty{conf: c, tmpl: t, logger: l}
if c.ServiceKey != "" {
n.apiV1 = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
}
return n
} }
const ( const (
@ -533,12 +545,6 @@ func (n *PagerDuty) notifyV1(
Details: details, Details: details,
} }
apiURL, err := url.Parse("https://events.pagerduty.com/generic/2010-04-15/create_event.json")
if err != nil {
return false, err
}
n.conf.URL = &config.URL{apiURL}
if eventType == pagerDutyEventTrigger { if eventType == pagerDutyEventTrigger {
msg.Client = tmpl(n.conf.Client) msg.Client = tmpl(n.conf.Client)
msg.ClientURL = tmpl(n.conf.ClientURL) msg.ClientURL = tmpl(n.conf.ClientURL)
@ -553,7 +559,7 @@ func (n *PagerDuty) notifyV1(
return false, err return false, err
} }
resp, err := post(ctx, c, n.conf.URL.String(), contentTypeJSON, &buf) resp, err := post(ctx, c, n.apiV1, contentTypeJSON, &buf)
if err != nil { if err != nil {
return true, err return true, err
} }
@ -664,7 +670,7 @@ func (n *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) (bool, error
return false, err return false, err
} }
if n.conf.ServiceKey != "" { if n.apiV1 != "" {
return n.notifyV1(ctx, c, eventType, key, data, details, as...) return n.notifyV1(ctx, c, eventType, key, data, details, as...)
} }
return n.notifyV2(ctx, c, eventType, key, data, details, as...) return n.notifyV2(ctx, c, eventType, key, data, details, as...)
@ -838,9 +844,10 @@ func (n *Slack) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
return false, err return false, err
} }
resp, err := post(ctx, c, n.conf.APIURL.String(), contentTypeJSON, &buf) u := n.conf.APIURL.String()
resp, err := post(ctx, c, u, contentTypeJSON, &buf)
if err != nil { if err != nil {
return true, err return true, redactURL(err)
} }
resp.Body.Close() resp.Body.Close()
@ -927,7 +934,7 @@ func (n *Hipchat) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
resp, err := post(ctx, c, apiURL.String(), contentTypeJSON, &buf) resp, err := post(ctx, c, apiURL.String(), contentTypeJSON, &buf)
if err != nil { if err != nil {
return true, err return true, redactURL(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -1028,7 +1035,7 @@ func (n *Wechat) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
resp, err := c.Do(req.WithContext(ctx)) resp, err := c.Do(req.WithContext(ctx))
if err != nil { if err != nil {
return true, err return true, redactURL(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -1079,7 +1086,7 @@ func (n *Wechat) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
resp, err := c.Do(req.WithContext(ctx)) resp, err := c.Do(req.WithContext(ctx))
if err != nil { if err != nil {
return true, err return true, redactURL(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -1308,7 +1315,7 @@ func (n *VictorOps) Notify(ctx context.Context, as ...*types.Alert) (bool, error
resp, err := post(ctx, c, apiURL.String(), contentTypeJSON, buf) resp, err := post(ctx, c, apiURL.String(), contentTypeJSON, buf)
if err != nil { if err != nil {
return true, err return true, redactURL(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -1396,11 +1403,12 @@ type Pushover struct {
conf *config.PushoverConfig conf *config.PushoverConfig
tmpl *template.Template tmpl *template.Template
logger log.Logger logger log.Logger
apiURL string // for tests.
} }
// NewPushover returns a new Pushover notifier. // NewPushover returns a new Pushover notifier.
func NewPushover(c *config.PushoverConfig, t *template.Template, l log.Logger) *Pushover { func NewPushover(c *config.PushoverConfig, t *template.Template, l log.Logger) *Pushover {
return &Pushover{conf: c, tmpl: t, logger: l} return &Pushover{conf: c, tmpl: t, logger: l, apiURL: "https://api.pushover.net/1/messages.json"}
} }
// Notify implements the Notifier interface. // Notify implements the Notifier interface.
@ -1465,13 +1473,13 @@ func (n *Pushover) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return false, err return false, err
} }
apiURL := "https://api.pushover.net/1/messages.json" u, err := url.Parse(n.apiURL)
u, err := url.Parse(apiURL)
if err != nil { if err != nil {
return false, err return false, err
} }
u.RawQuery = parameters.Encode() u.RawQuery = parameters.Encode()
level.Debug(n.logger).Log("msg", "Sending Pushover message", "incident", key, "url", u.String()) // Don't log the URL as it contains secret data (see #1825).
level.Debug(n.logger).Log("msg", "Sending Pushover message", "incident", key)
c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "pushover") c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "pushover")
if err != nil { if err != nil {
@ -1480,7 +1488,7 @@ func (n *Pushover) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
resp, err := post(ctx, c, u.String(), "text/plain", nil) resp, err := post(ctx, c, u.String(), "text/plain", nil)
if err != nil { if err != nil {
return true, err return true, redactURL(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -1559,6 +1567,16 @@ func hashKey(s string) string {
return fmt.Sprintf("%x", h.Sum(nil)) return fmt.Sprintf("%x", h.Sum(nil))
} }
// redactURL removes the URL part from an error of *url.Error type.
func redactURL(err error) error {
e, ok := err.(*url.Error)
if !ok {
return err
}
e.URL = "<redacted>"
return e
}
func post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) { func post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest("POST", url, body) req, err := http.NewRequest("POST", url, body)
if err != nil { if err != nil {

View File

@ -19,19 +19,76 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"testing" "testing"
"time" "time"
"github.com/go-kit/kit/log" "github.com/go-kit/kit/log"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types" "github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
) )
// getContextWithCancelingURL returns a context that gets canceled when a
// client does a GET request to the returned URL.
// Handlers passed to the function will be invoked in order before the context gets canceled.
func getContextWithCancelingURL(h ...func(w http.ResponseWriter, r *http.Request)) (context.Context, *url.URL, func()) {
done := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
i := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if i < len(h) {
h[i](w, r)
} else {
cancel()
<-done
}
i++
}))
// No need to check the error since httptest.NewServer always return a valid URL.
u, _ := url.Parse(srv.URL)
return ctx, u, func() {
close(done)
srv.Close()
}
}
// assertNotifyLeaksNoSecret calls the Notify() method of the notifier, expects
// it to fail because the context is canceled by the server and checks that no
// secret data is leaked in the error message returned by Notify().
func assertNotifyLeaksNoSecret(t *testing.T, ctx context.Context, n Notifier, secret ...string) {
t.Helper()
require.NotEmpty(t, secret)
ctx = WithGroupKey(ctx, "1")
ok, err := n.Notify(ctx, []*types.Alert{
&types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"lbl1": "val1",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
},
}...)
require.Error(t, err)
require.Contains(t, err.Error(), context.Canceled.Error())
for _, s := range secret {
require.NotContains(t, err.Error(), s)
}
require.True(t, ok)
}
func TestWebhookRetry(t *testing.T) { func TestWebhookRetry(t *testing.T) {
u, err := url.Parse("http://example.com") u, err := url.Parse("http://example.com")
if err != nil { if err != nil {
@ -67,6 +124,42 @@ func TestPagerDutyRetryV2(t *testing.T) {
} }
} }
func TestPagerDutyRedactedURLV1(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
key := "01234567890123456789012345678901"
notifier := NewPagerDuty(
&config.PagerdutyConfig{
ServiceKey: config.Secret(key),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
createTmpl(t),
log.NewNopLogger(),
)
notifier.apiV1 = u.String()
assertNotifyLeaksNoSecret(t, ctx, notifier, key)
}
func TestPagerDutyRedactedURLV2(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
key := "01234567890123456789012345678901"
notifier := NewPagerDuty(
&config.PagerdutyConfig{
URL: &config.URL{u},
RoutingKey: config.Secret(key),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
createTmpl(t),
log.NewNopLogger(),
)
assertNotifyLeaksNoSecret(t, ctx, notifier, key)
}
func TestSlackRetry(t *testing.T) { func TestSlackRetry(t *testing.T) {
notifier := new(Slack) notifier := new(Slack)
for statusCode, expected := range retryTests(defaultRetryCodes()) { for statusCode, expected := range retryTests(defaultRetryCodes()) {
@ -75,6 +168,22 @@ func TestSlackRetry(t *testing.T) {
} }
} }
func TestSlackRedactedURL(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
notifier := NewSlack(
&config.SlackConfig{
APIURL: &config.SecretURL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
createTmpl(t),
log.NewNopLogger(),
)
assertNotifyLeaksNoSecret(t, ctx, notifier, u.String())
}
func TestHipchatRetry(t *testing.T) { func TestHipchatRetry(t *testing.T) {
notifier := new(Hipchat) notifier := new(Hipchat)
retryCodes := append(defaultRetryCodes(), http.StatusTooManyRequests) retryCodes := append(defaultRetryCodes(), http.StatusTooManyRequests)
@ -84,6 +193,24 @@ func TestHipchatRetry(t *testing.T) {
} }
} }
func TestHipchatRedactedURL(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
token := "secret_token"
notifier := NewHipchat(
&config.HipchatConfig{
APIURL: &config.URL{URL: u},
AuthToken: config.Secret(token),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
createTmpl(t),
log.NewNopLogger(),
)
assertNotifyLeaksNoSecret(t, ctx, notifier, token)
}
func TestOpsGenieRetry(t *testing.T) { func TestOpsGenieRetry(t *testing.T) {
notifier := new(OpsGenie) notifier := new(OpsGenie)
@ -94,6 +221,24 @@ func TestOpsGenieRetry(t *testing.T) {
} }
} }
func TestOpsGenieRedactedURL(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
key := "key"
notifier := NewOpsGenie(
&config.OpsGenieConfig{
APIURL: &config.URL{URL: u},
APIKey: config.Secret(key),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
createTmpl(t),
log.NewNopLogger(),
)
assertNotifyLeaksNoSecret(t, ctx, notifier, key)
}
func TestVictorOpsRetry(t *testing.T) { func TestVictorOpsRetry(t *testing.T) {
notifier := new(VictorOps) notifier := new(VictorOps)
for statusCode, expected := range retryTests(defaultRetryCodes()) { for statusCode, expected := range retryTests(defaultRetryCodes()) {
@ -102,6 +247,24 @@ func TestVictorOpsRetry(t *testing.T) {
} }
} }
func TestVictorOpsRedactedURL(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
secret := "secret"
notifier := NewVictorOps(
&config.VictorOpsConfig{
APIURL: &config.URL{URL: u},
APIKey: config.Secret(secret),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
createTmpl(t),
log.NewNopLogger(),
)
assertNotifyLeaksNoSecret(t, ctx, notifier, secret)
}
func TestPushoverRetry(t *testing.T) { func TestPushoverRetry(t *testing.T) {
notifier := new(Pushover) notifier := new(Pushover)
for statusCode, expected := range retryTests(defaultRetryCodes()) { for statusCode, expected := range retryTests(defaultRetryCodes()) {
@ -110,6 +273,25 @@ func TestPushoverRetry(t *testing.T) {
} }
} }
func TestPushoverRedactedURL(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
key, token := "user_key", "token"
notifier := NewPushover(
&config.PushoverConfig{
UserKey: config.Secret(key),
Token: config.Secret(token),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
createTmpl(t),
log.NewNopLogger(),
)
notifier.apiURL = u.String()
assertNotifyLeaksNoSecret(t, ctx, notifier, key, token)
}
func retryTests(retryCodes []int) map[int]bool { func retryTests(retryCodes []int) map[int]bool {
tests := map[int]bool{ tests := map[int]bool{
// 1xx // 1xx
@ -295,7 +477,7 @@ func TestOpsGenie(t *testing.T) {
func TestEmailConfigNoAuthMechs(t *testing.T) { func TestEmailConfigNoAuthMechs(t *testing.T) {
email := &Email{ email := &Email{
conf: &config.EmailConfig{}, tmpl: &template.Template{}, logger: log.NewNopLogger(), conf: &config.EmailConfig{AuthUsername: "test"}, tmpl: &template.Template{}, logger: log.NewNopLogger(),
} }
_, err := email.auth("") _, err := email.auth("")
require.Error(t, err) require.Error(t, err)
@ -304,8 +486,9 @@ func TestEmailConfigNoAuthMechs(t *testing.T) {
func TestEmailConfigMissingAuthParam(t *testing.T) { func TestEmailConfigMissingAuthParam(t *testing.T) {
conf := &config.EmailConfig{AuthUsername: "test"}
email := &Email{ email := &Email{
conf: &config.EmailConfig{}, tmpl: &template.Template{}, logger: log.NewNopLogger(), conf: conf, tmpl: &template.Template{}, logger: log.NewNopLogger(),
} }
_, err := email.auth("CRAM-MD5") _, err := email.auth("CRAM-MD5")
require.Error(t, err) require.Error(t, err)
@ -324,6 +507,15 @@ func TestEmailConfigMissingAuthParam(t *testing.T) {
require.Equal(t, err.Error(), "missing password for PLAIN auth mechanism; missing password for LOGIN auth mechanism") require.Equal(t, err.Error(), "missing password for PLAIN auth mechanism; missing password for LOGIN auth mechanism")
} }
func TestEmailNoUsernameStillOk(t *testing.T) {
email := &Email{
conf: &config.EmailConfig{}, tmpl: &template.Template{}, logger: log.NewNopLogger(),
}
a, err := email.auth("CRAM-MD5")
require.NoError(t, err)
require.Nil(t, a)
}
func TestVictorOpsCustomFields(t *testing.T) { func TestVictorOpsCustomFields(t *testing.T) {
logger := log.NewNopLogger() logger := log.NewNopLogger()
tmpl := createTmpl(t) tmpl := createTmpl(t)
@ -371,3 +563,43 @@ func TestVictorOpsCustomFields(t *testing.T) {
// Verify that a custom field was added to the payload and templatized. // Verify that a custom field was added to the payload and templatized.
require.Equal(t, "message", m["Field_A"]) require.Equal(t, "message", m["Field_A"])
} }
func TestWechatRedactedURLOnInitialAuthentication(t *testing.T) {
ctx, u, fn := getContextWithCancelingURL()
defer fn()
secret := "secret_key"
notifier := NewWechat(
&config.WechatConfig{
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
CorpID: "corpid",
APISecret: config.Secret(secret),
},
createTmpl(t),
log.NewNopLogger(),
)
assertNotifyLeaksNoSecret(t, ctx, notifier, secret)
}
func TestWechatRedactedURLOnNotify(t *testing.T) {
secret, token := "secret", "token"
ctx, u, fn := getContextWithCancelingURL(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"access_token":"%s"}`, token)
})
defer fn()
notifier := NewWechat(
&config.WechatConfig{
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
CorpID: "corpid",
APISecret: config.Secret(secret),
},
createTmpl(t),
log.NewNopLogger(),
)
assertNotifyLeaksNoSecret(t, ctx, notifier, secret, token)
}

View File

@ -3,8 +3,8 @@
fmt.Fprintf fmt.Fprintf
fmt.Fprintln fmt.Fprintln
// Exclude both vendored and un-vendored to cover both vendor mode and module mode.
(github.com/prometheus/alertmanager/vendor/github.com/go-kit/kit/log.Logger).Log (github.com/prometheus/alertmanager/vendor/github.com/go-kit/kit/log.Logger).Log
// Allowed log levels are enforced via kingpin (github.com/go-kit/kit/log.Logger).Log
(*github.com/prometheus/alertmanager/vendor/github.com/prometheus/common/promlog.AllowedLevel).Set
// Generated via go-swagger // Generated via go-swagger
(*github.com/prometheus/alertmanager/api/v2/restapi.Server).Shutdown (*github.com/prometheus/alertmanager/api/v2/restapi.Server).Shutdown