Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMTP config: add global and local password file fields #3038

Merged
merged 13 commits into from Sep 16, 2022
Merged
48 changes: 28 additions & 20 deletions config/config.go
Expand Up @@ -335,6 +335,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
return fmt.Errorf("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured")
}

if len(c.Global.SMTPAuthPassword) > 0 && len(c.Global.SMTPAuthPasswordFile) > 0 {
return fmt.Errorf("at most one of smtp_auth_password & smtp_auth_password_file must be configured")
}

names := map[string]struct{}{}

for _, rcv := range c.Receivers {
Expand Down Expand Up @@ -368,6 +372,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if ec.AuthPassword == "" {
ec.AuthPassword = c.Global.SMTPAuthPassword
}
if ec.AuthPasswordFile == "" {
ec.AuthPasswordFile = c.Global.SMTPAuthPasswordFile
}
if ec.AuthSecret == "" {
ec.AuthSecret = c.Global.SMTPAuthSecret
}
Expand Down Expand Up @@ -693,26 +700,27 @@ type GlobalConfig struct {

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"`
SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"`
SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"`
SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"`
SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"`
SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"`
SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"`
SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"`
SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"`
SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"`
PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"`
OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"`
OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"`
OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"`
WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"`
WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"`
WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"`
VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"`
VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"`
TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"`
SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"`
SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"`
SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"`
SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"`
SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"`
SMTPAuthPasswordFile string `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"`
SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"`
SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"`
SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"`
SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"`
SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"`
PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"`
OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"`
OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"`
OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"`
WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"`
WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"`
WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"`
VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"`
VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"`
TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig.
Expand Down
28 changes: 28 additions & 0 deletions config/config_test.go
Expand Up @@ -965,6 +965,34 @@ func TestSMTPHello(t *testing.T) {
}
}

func TestSMTPBothPasswordAndFile(t *testing.T) {
_, err := LoadFile("testdata/conf.smtp-both-password-and-file.yml")
if err == nil {
t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.smtp-both-password-and-file.yml", err)
}
if err.Error() != "at most one of smtp_auth_password & smtp_auth_password_file must be configured" {
t.Errorf("Expected: %s\nGot: %s", "at most one of auth_password & auth_password_file must be configured", err.Error())
}
}

func TestSMTPNoUsernameOrPassword(t *testing.T) {
_, err := LoadFile("testdata/conf.smtp-no-username-or-password.yml")
if err != nil {
t.Fatalf("Error parsing %s: %s", "testdata/conf.smtp-no-username-or-password.yml", err)
}
}

func TestGlobalAndLocalSMTPPassword(t *testing.T) {
config, err := LoadFile("testdata/conf.smtp-password-global-and-local.yml")
if err != nil {
t.Fatalf("Error parsing %s: %s", "testdata/conf.smtp-password-global-and-local.yml", err)
}

require.Equal(t, "/tmp/globaluserpassword", config.Receivers[0].EmailConfigs[0].AuthPasswordFile, "first email should use password file /tmp/globaluserpassword")
require.Equal(t, "/tmp/localuser1password", config.Receivers[0].EmailConfigs[1].AuthPasswordFile, "second email should use password file /tmp/localuser1password")
require.Equal(t, Secret("mysecret"), config.Receivers[0].EmailConfigs[2].AuthPassword, "third email should use password mysecret")
}

func TestGroupByAll(t *testing.T) {
c, err := LoadFile("testdata/conf.group-by-all.yml")
if err != nil {
Expand Down
27 changes: 14 additions & 13 deletions config/notifiers.go
Expand Up @@ -165,19 +165,20 @@ type EmailConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`

// Email address to notify.
To string `yaml:"to,omitempty" json:"to,omitempty"`
From string `yaml:"from,omitempty" json:"from,omitempty"`
Hello string `yaml:"hello,omitempty" json:"hello,omitempty"`
Smarthost HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"`
AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"`
AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"`
AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"`
AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"`
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
HTML string `yaml:"html,omitempty" json:"html,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"`
TLSConfig commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
To string `yaml:"to,omitempty" json:"to,omitempty"`
From string `yaml:"from,omitempty" json:"from,omitempty"`
Hello string `yaml:"hello,omitempty" json:"hello,omitempty"`
Smarthost HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"`
AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"`
AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"`
AuthPasswordFile string `yaml:"auth_password_file,omitempty" json:"auth_password_file,omitempty"`
AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"`
AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"`
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
HTML string `yaml:"html,omitempty" json:"html,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"`
TLSConfig commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
Expand Down
13 changes: 13 additions & 0 deletions config/testdata/conf.smtp-both-password-and-file.yml
@@ -0,0 +1,13 @@
global:
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'
smtp_auth_username: 'alertmanager'
smtp_auth_password: "multiline\nmysecret"
smtp_auth_password_file: "/tmp/global"
smtp_hello: "host.example.org"
route:
receiver: 'email-notifications'
receivers:
- name: 'email-notifications'
email_configs:
- to: 'one@example.org'
10 changes: 10 additions & 0 deletions config/testdata/conf.smtp-no-username-or-password.yml
@@ -0,0 +1,10 @@
global:
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'
smtp_hello: "host.example.org"
route:
receiver: 'email-notifications'
receivers:
- name: 'email-notifications'
email_configs:
- to: 'one@example.org'
21 changes: 21 additions & 0 deletions config/testdata/conf.smtp-password-global-and-local.yml
@@ -0,0 +1,21 @@
global:
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'
smtp_auth_username: 'globaluser'
smtp_auth_password_file: '/tmp/globaluserpassword'
smtp_hello: "host.example.org"
route:
receiver: 'email-notifications'
receivers:
- name: 'email-notifications'
email_configs:
# Use global
- to: 'one@example.org'
# Override global with other file
- to: 'two@example.org'
auth_username: 'localuser1'
auth_password_file: '/tmp/localuser1password'
# Override global with inline password
- to: 'three@example.org'
auth_username: 'localuser2'
auth_password: 'mysecret'
4 changes: 4 additions & 0 deletions docs/configuration.md
Expand Up @@ -71,6 +71,8 @@ global:
[ smtp_auth_username: <string> ]
# SMTP Auth using LOGIN and PLAIN.
[ smtp_auth_password: <secret> ]
# SMTP Auth using LOGIN and PLAIN.
[ smtp_auth_password_file: <string> ]
# SMTP Auth using PLAIN.
[ smtp_auth_identity: <string> ]
# SMTP Auth using CRAM-MD5.
Expand Down Expand Up @@ -514,8 +516,10 @@ to: <tmpl_string>
[ hello: <string> | default = global.smtp_hello ]

# SMTP authentication information.
ericrrath marked this conversation as resolved.
Show resolved Hide resolved
# auth_password and auth_password_file are mutually exclusive.
[ auth_username: <string> | default = global.smtp_auth_username ]
[ auth_password: <secret> | default = global.smtp_auth_password ]
[ auth_password_file: <string> | default = global.smtp_auth_password_file ]
[ auth_secret: <secret> | default = global.smtp_auth_secret ]
[ auth_identity: <string> | default = global.smtp_auth_identity ]

Expand Down
26 changes: 24 additions & 2 deletions notify/email/email.go
Expand Up @@ -91,7 +91,11 @@ func (n *Email) auth(mechs string) (smtp.Auth, error) {
return smtp.CRAMMD5Auth(username, secret), nil

case "PLAIN":
password := string(n.conf.AuthPassword)
password, passwordErr := n.getPassword()
if passwordErr != nil {
err.Add(passwordErr)
continue
}
if password == "" {
err.Add(errors.New("missing password for PLAIN auth mechanism"))
continue
Expand All @@ -100,7 +104,11 @@ func (n *Email) auth(mechs string) (smtp.Auth, error) {

return smtp.PlainAuth(identity, username, password, n.conf.Smarthost.Host), nil
case "LOGIN":
password := string(n.conf.AuthPassword)
password, passwordErr := n.getPassword()
if passwordErr != nil {
err.Add(passwordErr)
continue
}
if password == "" {
err.Add(errors.New("missing password for LOGIN auth mechanism"))
continue
Expand Down Expand Up @@ -353,3 +361,17 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
}
return nil, nil
}

func (n *Email) getPassword() (string, error) {
if len(n.conf.AuthPassword) > 0 {
return string(n.conf.AuthPassword), nil
}
if len(n.conf.AuthPasswordFile) > 0 {
content, err := os.ReadFile(n.conf.AuthPasswordFile)
if err != nil {
return "", err
}
return string(content), nil
}
return "", nil
}
27 changes: 27 additions & 0 deletions notify/email/email_test.go
Expand Up @@ -427,6 +427,16 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
t.Fatal(err)
}

fileWithCorrectPassword, err := os.CreateTemp("", "smtp-password-correct")
require.NoError(t, err, "creating temp file failed")
_, err = fileWithCorrectPassword.WriteString(c.Password)
require.NoError(t, err, "writing to temp file failed")

fileWithIncorrectPassword, err := os.CreateTemp("", "smtp-password-incorrect")
require.NoError(t, err, "creating temp file failed")
_, err = fileWithIncorrectPassword.WriteString(c.Password + "wrong")
require.NoError(t, err, "writing to temp file failed")

for _, tc := range []struct {
title string
updateCfg func(*config.EmailConfig)
Expand All @@ -441,6 +451,13 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
cfg.AuthPassword = config.Secret(c.Password)
},
},
{
title: "email with authentication (password from file)",
updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username
cfg.AuthPasswordFile = fileWithCorrectPassword.Name()
},
},
{
title: "HTML-only email",
updateCfg: func(cfg *config.EmailConfig) {
Expand Down Expand Up @@ -486,6 +503,16 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
errMsg: "Invalid username or password",
retry: true,
},
{
title: "wrong credentials (password from file)",
updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username
cfg.AuthPasswordFile = fileWithIncorrectPassword.Name()
},

errMsg: "Invalid username or password",
retry: true,
},
{
title: "no credentials",
errMsg: "authentication Required",
Expand Down