diff --git a/config/config.go b/config/config.go index 4f8d803e63..19db785bb9 100644 --- a/config/config.go +++ b/config/config.go @@ -419,6 +419,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error { return errors.New("at most one of rocketchat_token_id & rocketchat_token_id_file must be configured") } + if c.Global.WeChatAPISecret != "" && len(c.Global.WeChatAPISecretFile) > 0 { + return errors.New("at most one of wechat_api_secret & wechat_api_secret_file must be configured") + } + names := map[string]struct{}{} for _, rcv := range c.Receivers { @@ -557,11 +561,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error { wcc.APIURL = c.Global.WeChatAPIURL } - if wcc.APISecret == "" { - if c.Global.WeChatAPISecret == "" { - return errors.New("no global Wechat ApiSecret set") + if wcc.APISecret == "" && len(wcc.APISecretFile) == 0 { + if c.Global.WeChatAPISecret == "" && len(c.Global.WeChatAPISecretFile) == 0 { + return errors.New("no global Wechat Api Secret set either inline or in a file") } wcc.APISecret = c.Global.WeChatAPISecret + wcc.APISecretFile = c.Global.WeChatAPISecretFile } if wcc.CorpID == "" { @@ -912,6 +917,7 @@ type GlobalConfig struct { 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"` + WeChatAPISecretFile string `yaml:"wechat_api_secret_file,omitempty" json:"wechat_api_secret_file,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"` diff --git a/config/config_test.go b/config/config_test.go index 17ad652b6f..f53603d3fa 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1593,3 +1593,48 @@ func TestInhibitRuleEqual(t *testing.T) { r = c.InhibitRules[0] require.Equal(t, []string{"qux🙂", "corge"}, r.Equal) } + +func TestWechatNoAPIURL(t *testing.T) { + _, err := LoadFile("testdata/conf.wechat-no-api-secret.yml") + if err == nil { + t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.wechat-no-api-url.yml", err) + } + if err.Error() != "no global Wechat Api Secret set either inline or in a file" { + t.Errorf("Expected: %s\nGot: %s", "no global Wechat Api Secret set either inline or in a file", err.Error()) + } +} + +func TestWechatBothAPIURLAndFile(t *testing.T) { + _, err := LoadFile("testdata/conf.wechat-both-file-and-secret.yml") + if err == nil { + t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.wechat-both-file-and-secret.yml", err) + } + if err.Error() != "at most one of wechat_api_secret & wechat_api_secret_file must be configured" { + t.Errorf("Expected: %s\nGot: %s", "at most one of wechat_api_secret & wechat_api_secret_file must be configured", err.Error()) + } +} + +func TestWechatGlobalAPISecretFile(t *testing.T) { + conf, err := LoadFile("testdata/conf.wechat-default-api-secret-file.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.wechat-default-api-secret-file.yml", err) + } + + // no override + firstConfig := conf.Receivers[0].WechatConfigs[0] + if firstConfig.APISecretFile != "/global_file" || string(firstConfig.APISecret) != "" { + t.Fatalf("Invalid Wechat API Secret file: %s\nExpected: %s", firstConfig.APISecretFile, "/global_file") + } + + // override the file + secondConfig := conf.Receivers[0].WechatConfigs[1] + if secondConfig.APISecretFile != "/override_file" || string(secondConfig.APISecret) != "" { + t.Fatalf("Invalid Wechat API Secret file: %s\nExpected: %s", secondConfig.APISecretFile, "/override_file") + } + + // override the global file with an inline URL + thirdConfig := conf.Receivers[0].WechatConfigs[2] + if string(thirdConfig.APISecret) != "my_inline_secret" || thirdConfig.APISecretFile != "" { + t.Fatalf("Invalid Wechat API Secret: %s\nExpected: %s", string(thirdConfig.APISecret), "my_inline_secret") + } +} diff --git a/config/notifiers.go b/config/notifiers.go index bf7be0b28a..7f21042a2a 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -665,15 +665,16 @@ type WechatConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` - APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"` - CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"` - Message string `yaml:"message,omitempty" json:"message,omitempty"` - APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` - ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"` - ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"` - ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"` - AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"` - MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"` + APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"` + APISecretFile string `yaml:"api_secret_file,omitempty" json:"api_secret_file,omitempty"` + CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` + ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"` + ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"` + ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"` + AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"` + MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"` } const wechatValidTypesRe = `^(text|markdown)$` @@ -696,6 +697,10 @@ func (c *WechatConfig) UnmarshalYAML(unmarshal func(any) error) error { return fmt.Errorf("weChat message type %q does not match valid options %s", c.MessageType, wechatValidTypesRe) } + if c.APISecret != "" && len(c.APISecretFile) > 0 { + return errors.New("at most one of api_secret & api_secret_file must be configured") + } + return nil } diff --git a/config/testdata/conf.wechat-both-file-and-secret.yml b/config/testdata/conf.wechat-both-file-and-secret.yml new file mode 100644 index 0000000000..0bdb689cc1 --- /dev/null +++ b/config/testdata/conf.wechat-both-file-and-secret.yml @@ -0,0 +1,10 @@ +global: + wechat_api_secret: "http://mysecret.example.com/" + wechat_api_secret_file: '/global_file' +route: + receiver: 'wechat-notifications' + group_by: [alertname, datacenter, app] +receivers: + - name: 'wechat-notifications' + wechat_configs: + - {} diff --git a/config/testdata/conf.wechat-default-api-secret-file.yml b/config/testdata/conf.wechat-default-api-secret-file.yml new file mode 100644 index 0000000000..3864cd5a89 --- /dev/null +++ b/config/testdata/conf.wechat-default-api-secret-file.yml @@ -0,0 +1,15 @@ +global: + wechat_api_secret_file: '/global_file' + wechat_api_corp_id: 'my_corp_id' +route: + receiver: 'wechat-notifications' + group_by: [alertname, datacenter, app] +receivers: + - name: 'wechat-notifications' + wechat_configs: + # Use global + - {} + # Override global with other file + - api_secret_file: '/override_file' + # Override global with inline API secret + - api_secret: 'my_inline_secret' diff --git a/config/testdata/conf.wechat-no-api-secret.yml b/config/testdata/conf.wechat-no-api-secret.yml new file mode 100644 index 0000000000..887d93143c --- /dev/null +++ b/config/testdata/conf.wechat-no-api-secret.yml @@ -0,0 +1,7 @@ +route: + receiver: 'wechat-notifications' + group_by: [alertname, datacenter, app] +receivers: + - name: 'wechat-notifications' + wechat_configs: + - {} diff --git a/docs/configuration.md b/docs/configuration.md index 500f527064..ad1c2f6db8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -116,6 +116,7 @@ global: [ rocketchat_token_id_file: ] [ wechat_api_url: | default = "https://qyapi.weixin.qq.com/cgi-bin/" ] [ wechat_api_secret: ] + [ wechat_api_secret_file: ] [ wechat_api_corp_id: ] [ telegram_api_url: | default = "https://api.telegram.org" ] [ webex_api_url: | default = "https://webexapis.com/v1/messages" ] @@ -1875,8 +1876,9 @@ API](https://developers.weixin.qq.com/doc/offiaccount/en/Message_Management/Serv # Whether to notify about resolved alerts. [ send_resolved: | default = false ] -# The API key to use when talking to the WeChat API. +# The API key to use when talking to the WeChat API. Either api_secret or api_secret_file should be set. [ api_secret: | default = global.wechat_api_secret ] +[ api_secret_file: | default = global.wechat_api_secret_file ] # The WeChat API URL. [ api_url: | default = global.wechat_api_url ] diff --git a/notify/wechat/wechat.go b/notify/wechat/wechat.go index 79ae2e2951..b02d04a376 100644 --- a/notify/wechat/wechat.go +++ b/notify/wechat/wechat.go @@ -23,6 +23,8 @@ import ( "log/slog" "net/http" "net/url" + "os" + "strings" "time" commoncfg "github.com/prometheus/common/config" @@ -99,7 +101,11 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) // Refresh AccessToken over 2 hours if n.accessToken == "" || time.Since(n.accessTokenAt) > 2*time.Hour { parameters := url.Values{} - parameters.Add("corpsecret", tmpl(string(n.conf.APISecret))) + apiSecret, err := n.getApiSecret() + if err != nil { + return false, err + } + parameters.Add("corpsecret", tmpl(apiSecret)) parameters.Add("corpid", tmpl(string(n.conf.CorpID))) if err != nil { return false, fmt.Errorf("templating error: %w", err) @@ -196,3 +202,14 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) return false, errors.New(weResp.Error) } + +func (n *Notifier) getApiSecret() (string, error) { + if len(n.conf.APISecretFile) > 0 { + content, err := os.ReadFile(n.conf.APISecretFile) + if err != nil { + return "", err + } + return strings.TrimSpace(string(content)), nil + } + return string(n.conf.APISecret), nil +} diff --git a/notify/wechat/wechat_test.go b/notify/wechat/wechat_test.go index bab4e933c1..fe9eee23be 100644 --- a/notify/wechat/wechat_test.go +++ b/notify/wechat/wechat_test.go @@ -16,6 +16,7 @@ package wechat import ( "fmt" "net/http" + "os" "testing" commoncfg "github.com/prometheus/common/config" @@ -90,3 +91,34 @@ func TestWechatMessageTypeSelector(t *testing.T) { test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret, token) } + +func TestGetApiSecretFromSecret(t *testing.T) { + n := &Notifier{conf: &config.WechatConfig{APISecret: config.Secret("shhh")}} + s, err := n.getApiSecret() + require.NoError(t, err) + require.Equal(t, "shhh", s) +} + +func TestGetApiSecretFromFile(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "wechat-secret-*") + require.NoError(t, err) + secretContent := "file-secret\n" + _, err = tmpFile.WriteString(secretContent) + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + n := &Notifier{conf: &config.WechatConfig{APISecretFile: tmpFile.Name()}} + s, err := n.getApiSecret() + require.NoError(t, err) + require.Equal(t, "file-secret", s) +} + +func TestGetApiSecretFromMissingFile(t *testing.T) { + n := &Notifier{conf: &config.WechatConfig{APISecretFile: "/non/existent/wechat-secret.txt"}} + s, err := n.getApiSecret() + var pathErr *os.PathError + require.ErrorAs(t, err, &pathErr) + require.Equal(t, "/non/existent/wechat-secret.txt", pathErr.Path) + require.ErrorIs(t, err, os.ErrNotExist) + require.Empty(t, s) +}