Alerting: Add OAuth2 Support for Webhook Receiver (#106302)
* Add to available channels * Export * Fix bug in deeply nested secrets BE: Slice re-use bug when traversing deeply. FE: Only at most one level of nesting was being taken into account when determining secureFields keys. This change adds a new field on NotificationChannelOption: secureFieldKey. This is populated on API GET via transform. This change gives us the option to hardcode secureFieldKey in the backend and no longer calculate the key via settings topology. * Update grafana/alerting to 3e20fda3b872 * Prettier * Linting * Fix IntegrationConfig test to catch secure field mismatch
This commit is contained in:
@@ -324,6 +324,7 @@ type WebhookIntegration struct {
|
||||
Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"`
|
||||
TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tlsConfig,block"`
|
||||
HMACConfig *HMACConfig `json:"hmacConfig,omitempty" yaml:"hmacConfig,omitempty" hcl:"hmacConfig,block"`
|
||||
HTTPConfig *HTTPClientConfig `json:"http_config,omitempty" yaml:"http_config,omitempty" hcl:"http_config,block"`
|
||||
|
||||
Payload *CustomPayload `json:"payload,omitempty" yaml:"payload,omitempty" hcl:"payload,block"`
|
||||
}
|
||||
@@ -343,6 +344,41 @@ type HMACConfig struct {
|
||||
TimestampHeader string `yaml:"timestampHeader,omitempty" json:"timestampHeader,omitempty" hcl:"timestamp_header"`
|
||||
}
|
||||
|
||||
// HTTPClientConfig holds common configurations for notifier HTTP clients.
|
||||
type HTTPClientConfig struct {
|
||||
OAuth2Config *OAuth2Config `json:"oauth2,omitempty" yaml:"oauth2,omitempty" hcl:"oauth2,block"`
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
// ProxyURL is the HTTP proxy server to use to connect to the targets.
|
||||
ProxyURL *string `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty" hcl:"proxy_url"`
|
||||
// NoProxy contains addresses that should not use a proxy.
|
||||
NoProxy *string `yaml:"no_proxy,omitempty" json:"no_proxy,omitempty" hcl:"no_proxy"`
|
||||
// ProxyFromEnvironment uses environment HTTP_PROXY, HTTPS_PROXY and NO_PROXY to determine proxies.
|
||||
ProxyFromEnvironment *bool `yaml:"proxy_from_environment,omitempty" json:"proxy_from_environment,omitempty" hcl:"proxy_from_environment"`
|
||||
// ProxyConnectHeader optionally specifies headers to send to proxies during CONNECT requests.
|
||||
ProxyConnectHeader *map[string]string `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty" hcl:"proxy_connect_header"`
|
||||
}
|
||||
|
||||
type OAuth2Config struct {
|
||||
// ClientID is the OAuth2 client ID.
|
||||
ClientID string `json:"client_id" yaml:"client_id" hcl:"client_id"`
|
||||
// ClientSecret is the OAuth2 client secret.
|
||||
ClientSecret *Secret `json:"client_secret" yaml:"client_secret" hcl:"client_secret"`
|
||||
// TokenURL is the URL to get the OAuth2 token.
|
||||
TokenURL string `json:"token_url" yaml:"token_url" hcl:"token_url"`
|
||||
|
||||
// Scopes is the optional list of OAuth2 scopes.
|
||||
Scopes *[]string `json:"scopes,omitempty" yaml:"scopes,omitempty" hcl:"scopes"`
|
||||
// EndpointParams is the optional map of additional parameters to include in the token request.
|
||||
EndpointParams *map[string]string `json:"endpoint_params,omitempty" yaml:"endpoint_params,omitempty" hcl:"endpoint_params"`
|
||||
// TLSConfig is the optional TLS configuration to use for the OAuth2 token request.
|
||||
TLSConfig *TLSConfig `json:"tls_config,omitempty" yaml:"tls_config,omitempty" hcl:"tls_config,block"`
|
||||
|
||||
// ProxyConfig is the optional proxy configuration to use for the OAuth2 token request.
|
||||
ProxyConfig *ProxyConfig `json:"proxy_config,omitempty" yaml:"proxy_config,omitempty" hcl:"proxy_config,block"`
|
||||
}
|
||||
|
||||
type WecomIntegration struct {
|
||||
DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"`
|
||||
|
||||
|
||||
@@ -191,8 +191,12 @@ func (f IntegrationFieldPath) String() string {
|
||||
return strings.Join(f, ".")
|
||||
}
|
||||
|
||||
func (f IntegrationFieldPath) Append(segment string) IntegrationFieldPath {
|
||||
return append(f, segment)
|
||||
func (f IntegrationFieldPath) With(segment string) IntegrationFieldPath {
|
||||
// Copy the existing path to avoid modifying the original slice.
|
||||
newPath := make(IntegrationFieldPath, len(f)+1)
|
||||
copy(newPath, f)
|
||||
newPath[len(newPath)-1] = segment
|
||||
return newPath
|
||||
}
|
||||
|
||||
// IntegrationConfigFromType returns an integration configuration for a given integration type. If the integration type is
|
||||
@@ -250,11 +254,12 @@ func (config *IntegrationConfig) GetSecretFields() []IntegrationFieldPath {
|
||||
func traverseFields(flds map[string]IntegrationField, parentPath IntegrationFieldPath, predicate func(i IntegrationField) bool) []IntegrationFieldPath {
|
||||
var result []IntegrationFieldPath
|
||||
for key, field := range flds {
|
||||
path := parentPath.With(key)
|
||||
if predicate(field) {
|
||||
result = append(result, parentPath.Append(key))
|
||||
result = append(result, path)
|
||||
}
|
||||
if len(field.Fields) > 0 {
|
||||
result = append(result, traverseFields(field.Fields, parentPath.Append(key), predicate)...)
|
||||
result = append(result, traverseFields(field.Fields, path, predicate)...)
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -244,11 +244,14 @@ func TestIntegrationConfig(t *testing.T) {
|
||||
allSecrets[key] = struct{}{}
|
||||
}
|
||||
|
||||
for field := range config.Fields {
|
||||
_, isSecret := allSecrets[field]
|
||||
assert.Equalf(t, isSecret, config.IsSecureField(NewIntegrationFieldPath(field)), "field '%s' is expected to be secret", field)
|
||||
secretFields := config.GetSecretFields()
|
||||
for _, path := range secretFields {
|
||||
_, isSecret := allSecrets[path.String()]
|
||||
assert.Equalf(t, isSecret, config.IsSecureField(path), "field '%s' is expected to be secret", path)
|
||||
delete(allSecrets, path.String())
|
||||
}
|
||||
assert.False(t, config.IsSecureField(IntegrationFieldPath{"__--**unknown_field**--__"}))
|
||||
assert.Empty(t, allSecrets, "mismatched secret fields for integration type %s: %v", integrationType, allSecrets)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,163 @@ func GetAvailableNotifiers() []*NotifierPlugin {
|
||||
},
|
||||
}
|
||||
|
||||
tlsSubformOptions := func() []NotifierOption {
|
||||
return []NotifierOption{
|
||||
{
|
||||
Label: "Disable certificate verification",
|
||||
Element: ElementTypeCheckbox,
|
||||
Description: "Do not verify the server's certificate chain and host name.",
|
||||
PropertyName: "insecureSkipVerify",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Label: "CA Certificate",
|
||||
Element: ElementTypeTextArea,
|
||||
Description: "Certificate in PEM format to use when verifying the server's certificate chain.",
|
||||
InputType: InputTypeText,
|
||||
PropertyName: "caCertificate",
|
||||
Required: false,
|
||||
Secure: true,
|
||||
},
|
||||
{
|
||||
Label: "Client Certificate",
|
||||
Element: ElementTypeTextArea,
|
||||
Description: "Client certificate in PEM format to use when connecting to the server.",
|
||||
InputType: InputTypeText,
|
||||
PropertyName: "clientCertificate",
|
||||
Required: false,
|
||||
Secure: true,
|
||||
},
|
||||
{
|
||||
Label: "Client Key",
|
||||
Element: ElementTypeTextArea,
|
||||
Description: "Client key in PEM format to use when connecting to the server.",
|
||||
InputType: InputTypeText,
|
||||
PropertyName: "clientKey",
|
||||
Required: false,
|
||||
Secure: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
proxyOption := func() NotifierOption {
|
||||
return NotifierOption{ // New in 12.1.
|
||||
Label: "Proxy Config",
|
||||
PropertyName: "proxy_config",
|
||||
Description: "Optional proxy configuration.",
|
||||
Element: ElementTypeSubform,
|
||||
SubformOptions: []NotifierOption{
|
||||
{
|
||||
Label: "Proxy URL",
|
||||
PropertyName: "proxy_url",
|
||||
Description: "HTTP proxy server to use to connect to the targets.",
|
||||
Element: ElementTypeInput,
|
||||
InputType: InputTypeText,
|
||||
Placeholder: "https://proxy.example.com",
|
||||
Required: false,
|
||||
Secure: false,
|
||||
},
|
||||
{
|
||||
Label: "Proxy from environment",
|
||||
PropertyName: "proxy_from_environment",
|
||||
Description: "Use environment HTTP_PROXY, HTTPS_PROXY and NO_PROXY to determine proxies.",
|
||||
Element: ElementTypeCheckbox,
|
||||
Required: false,
|
||||
Secure: false,
|
||||
},
|
||||
{
|
||||
Label: "No Proxy",
|
||||
PropertyName: "no_proxy",
|
||||
Description: "Comma-separated list of addresses that should not use a proxy.",
|
||||
Element: ElementTypeInput,
|
||||
InputType: InputTypeText,
|
||||
Placeholder: "example.com,1.2.3.4",
|
||||
Required: false,
|
||||
Secure: false,
|
||||
},
|
||||
{
|
||||
Label: "Proxy Connect Header",
|
||||
PropertyName: "proxy_connect_header",
|
||||
Description: "Optional headers to send to proxies during CONNECT requests.",
|
||||
Element: ElementTypeKeyValueMap,
|
||||
InputType: InputTypeText,
|
||||
Required: false,
|
||||
Secure: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
commonHttpClientOption := func() NotifierOption {
|
||||
return NotifierOption{ // New in 12.1.
|
||||
Label: "HTTP Config",
|
||||
PropertyName: "http_config",
|
||||
Description: "Common HTTP client options.",
|
||||
Element: ElementTypeSubform,
|
||||
SubformOptions: []NotifierOption{
|
||||
{ // New in 12.1.
|
||||
Label: "OAuth2",
|
||||
PropertyName: "oauth2",
|
||||
Description: "OAuth2 configuration options",
|
||||
Element: ElementTypeSubform,
|
||||
SubformOptions: []NotifierOption{
|
||||
{
|
||||
Label: "Token URL",
|
||||
PropertyName: "token_url",
|
||||
Element: ElementTypeInput,
|
||||
Description: "URL for the access token endpoint.",
|
||||
InputType: InputTypeText,
|
||||
Required: true,
|
||||
Secure: false,
|
||||
},
|
||||
{
|
||||
Label: "Client ID",
|
||||
PropertyName: "client_id",
|
||||
Element: ElementTypeInput,
|
||||
Description: "Client ID to use when authenticating.",
|
||||
InputType: InputTypeText,
|
||||
Required: true,
|
||||
Secure: false,
|
||||
},
|
||||
{
|
||||
Label: "Client Secret",
|
||||
PropertyName: "client_secret",
|
||||
Element: ElementTypeInput,
|
||||
Description: "Client secret to use when authenticating.",
|
||||
InputType: InputTypeText,
|
||||
Required: true,
|
||||
Secure: true,
|
||||
},
|
||||
{
|
||||
Label: "Scopes",
|
||||
PropertyName: "scopes",
|
||||
Element: ElementStringArray,
|
||||
Description: "Optional scopes to request when obtaining an access token.",
|
||||
Required: false,
|
||||
Secure: false,
|
||||
},
|
||||
{
|
||||
Label: "Endpoint Parameters",
|
||||
PropertyName: "endpoint_params",
|
||||
Element: ElementTypeKeyValueMap,
|
||||
Description: "Optional parameters to append to the access token request.",
|
||||
Required: false,
|
||||
Secure: false,
|
||||
},
|
||||
{
|
||||
Label: "TLS",
|
||||
PropertyName: "tls_config",
|
||||
Description: "Optional TLS configuration options for OAuth2 requests.",
|
||||
Element: ElementTypeSubform,
|
||||
SubformOptions: tlsSubformOptions(),
|
||||
},
|
||||
proxyOption(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return []*NotifierPlugin{
|
||||
{
|
||||
Type: "dingding",
|
||||
@@ -1006,46 +1163,11 @@ func GetAvailableNotifiers() []*NotifierPlugin {
|
||||
},
|
||||
|
||||
{
|
||||
Label: "TLS",
|
||||
PropertyName: "tlsConfig",
|
||||
Description: "TLS configuration options",
|
||||
Element: ElementTypeSubform,
|
||||
SubformOptions: []NotifierOption{
|
||||
{
|
||||
Label: "Disable certificate verification",
|
||||
Element: ElementTypeCheckbox,
|
||||
Description: "Do not verify the server's certificate chain and host name.",
|
||||
PropertyName: "insecureSkipVerify",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Label: "CA Certificate",
|
||||
Element: ElementTypeTextArea,
|
||||
Description: "Certificate in PEM format to use when verifying the server's certificate chain.",
|
||||
InputType: InputTypeText,
|
||||
PropertyName: "caCertificate",
|
||||
Required: false,
|
||||
Secure: true,
|
||||
},
|
||||
{
|
||||
Label: "Client Certificate",
|
||||
Element: ElementTypeTextArea,
|
||||
Description: "Client certificate in PEM format to use when connecting to the server.",
|
||||
InputType: InputTypeText,
|
||||
PropertyName: "clientCertificate",
|
||||
Required: false,
|
||||
Secure: true,
|
||||
},
|
||||
{
|
||||
Label: "Client Key",
|
||||
Element: ElementTypeTextArea,
|
||||
Description: "Client key in PEM format to use when connecting to the server.",
|
||||
InputType: InputTypeText,
|
||||
PropertyName: "clientKey",
|
||||
Required: false,
|
||||
Secure: true,
|
||||
},
|
||||
},
|
||||
Label: "TLS",
|
||||
PropertyName: "tlsConfig",
|
||||
Description: "TLS configuration options",
|
||||
Element: ElementTypeSubform,
|
||||
SubformOptions: tlsSubformOptions(),
|
||||
},
|
||||
{
|
||||
Label: "HMAC Signature",
|
||||
@@ -1083,6 +1205,7 @@ func GetAvailableNotifiers() []*NotifierPlugin {
|
||||
},
|
||||
},
|
||||
},
|
||||
commonHttpClientOption(), // New in 12.1.
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,7 +22,18 @@ func TestGetSecretKeysForContactPointType(t *testing.T) {
|
||||
{receiverType: "sensugo", expectedSecretFields: []string{"apikey"}},
|
||||
{receiverType: "teams", expectedSecretFields: []string{}},
|
||||
{receiverType: "telegram", expectedSecretFields: []string{"bottoken"}},
|
||||
{receiverType: "webhook", expectedSecretFields: []string{"password", "authorization_credentials", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey", "hmacConfig.secret"}},
|
||||
{receiverType: "webhook", expectedSecretFields: []string{
|
||||
"password",
|
||||
"authorization_credentials",
|
||||
"tlsConfig.caCertificate",
|
||||
"tlsConfig.clientCertificate",
|
||||
"tlsConfig.clientKey",
|
||||
"hmacConfig.secret",
|
||||
"http_config.oauth2.client_secret",
|
||||
"http_config.oauth2.tls_config.caCertificate",
|
||||
"http_config.oauth2.tls_config.clientCertificate",
|
||||
"http_config.oauth2.tls_config.clientKey",
|
||||
}},
|
||||
{receiverType: "wecom", expectedSecretFields: []string{"url", "secret"}},
|
||||
{receiverType: "prometheus-alertmanager", expectedSecretFields: []string{"basicAuthPassword"}},
|
||||
{receiverType: "discord", expectedSecretFields: []string{"url"}},
|
||||
|
||||
Reference in New Issue
Block a user