grafana/pkg/services/ngalert/models/receivers.go
Matthew Jacobson 3bf77d2e05
Alerting: Include in-use metadata in k8s receiver LIST & GET (#93016)
* Include in-use metadata in k8s receiver List & Get
2024-09-13 20:20:09 +03:00

643 lines
19 KiB
Go

package models
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"math"
"slices"
"sort"
"strings"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
)
// GetReceiverQuery represents a query for a single receiver.
type GetReceiverQuery struct {
OrgID int64
Name string
Decrypt bool
}
// GetReceiversQuery represents a query for receiver groups.
type GetReceiversQuery struct {
OrgID int64
Names []string
Limit int
Offset int
Decrypt bool
}
// ListReceiversQuery represents a query for listing receiver groups.
type ListReceiversQuery struct {
OrgID int64
Names []string
Limit int
Offset int
}
// ReceiverMetadata contains metadata about a receiver's usage in routes and rules.
type ReceiverMetadata struct {
InUseByRules []AlertRuleKey
InUseByRoutes int
}
// Receiver is the domain model representation of a receiver / contact point.
type Receiver struct {
UID string
Name string
Integrations []*Integration
Provenance Provenance
Version string
}
func (r *Receiver) Clone() Receiver {
clone := Receiver{
UID: r.UID,
Name: r.Name,
Provenance: r.Provenance,
Version: r.Version,
}
if r.Integrations != nil {
clone.Integrations = make([]*Integration, len(r.Integrations))
for i, integration := range r.Integrations {
cloneIntegration := integration.Clone()
clone.Integrations[i] = &cloneIntegration
}
}
return clone
}
// Encrypt encrypts all integrations.
func (r *Receiver) Encrypt(encryptFn EncryptFn) error {
for _, integration := range r.Integrations {
if err := integration.Encrypt(encryptFn); err != nil {
return err
}
}
return nil
}
// Decrypt decrypts all integrations.
func (r *Receiver) Decrypt(decryptFn DecryptFn) error {
var errs []error
for _, integration := range r.Integrations {
if err := integration.Decrypt(decryptFn); err != nil {
errs = append(errs, fmt.Errorf("failed to decrypt integration %s: %w", integration.UID, err))
}
}
return errors.Join(errs...)
}
// Redact redacts all integrations.
func (r *Receiver) Redact(redactFn RedactFn) {
for _, integration := range r.Integrations {
integration.Redact(redactFn)
}
}
// WithExistingSecureFields copies secure settings from an existing receivers for each integration. Which fields to copy
// is determined by the integrationSecureFields map, which contains a list of secure fields for each integration UID.
func (r *Receiver) WithExistingSecureFields(existing *Receiver, integrationSecureFields map[string][]string) {
existingIntegrations := make(map[string]*Integration, len(existing.Integrations))
for _, integration := range existing.Integrations {
existingIntegrations[integration.UID] = integration
}
for _, integration := range r.Integrations {
if integration.UID == "" {
// This is a new integration, so we don't need to copy any secure fields.
continue
}
fields := integrationSecureFields[integration.UID]
if len(fields) > 0 {
integration.WithExistingSecureFields(existingIntegrations[integration.UID], fields)
}
}
}
// Validate validates all integration settings, ensuring that the integrations are correctly configured.
func (r *Receiver) Validate(decryptFn DecryptFn) error {
var errs []error
for _, integration := range r.Integrations {
if err := integration.Validate(decryptFn); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// Integration is the domain model representation of an integration.
type Integration struct {
UID string
Name string
Config IntegrationConfig
DisableResolveMessage bool
// Settings can contain both secure and non-secure settings either unencrypted or redacted.
Settings map[string]any
// SecureSettings can contain only secure settings either encrypted or redacted.
SecureSettings map[string]string
}
// IntegrationConfig represents the configuration of an integration. It contains the type and information about the fields.
type IntegrationConfig struct {
Type string
Fields map[string]IntegrationField
}
// IntegrationField represents a field in an integration configuration.
type IntegrationField struct {
Name string
Fields map[string]IntegrationField
Secure bool
}
type IntegrationFieldPath []string
func NewIntegrationFieldPath(path string) IntegrationFieldPath {
return strings.Split(path, ".")
}
func (f IntegrationFieldPath) Head() string {
if len(f) > 0 {
return f[0]
}
return ""
}
func (f IntegrationFieldPath) Tail() IntegrationFieldPath {
return f[1:]
}
func (f IntegrationFieldPath) IsLeaf() bool {
return len(f) == 1
}
func (f IntegrationFieldPath) String() string {
return strings.Join(f, ".")
}
func (f IntegrationFieldPath) Append(segment string) IntegrationFieldPath {
return append(f, segment)
}
// IntegrationConfigFromType returns an integration configuration for a given integration type. If the integration type is
// not found an error is returned.
func IntegrationConfigFromType(integrationType string) (IntegrationConfig, error) {
config, err := channels_config.ConfigForIntegrationType(integrationType)
if err != nil {
return IntegrationConfig{}, err
}
integrationConfig := IntegrationConfig{Type: config.Type, Fields: make(map[string]IntegrationField, len(config.Options))}
for _, option := range config.Options {
integrationConfig.Fields[option.PropertyName] = notifierOptionToIntegrationField(option)
}
return integrationConfig, nil
}
func notifierOptionToIntegrationField(option channels_config.NotifierOption) IntegrationField {
f := IntegrationField{
Name: option.PropertyName,
Secure: option.Secure,
Fields: make(map[string]IntegrationField, len(option.SubformOptions)),
}
for _, subformOption := range option.SubformOptions {
f.Fields[subformOption.PropertyName] = notifierOptionToIntegrationField(subformOption)
}
return f
}
// IsSecureField returns true if the field is both known and marked as secure in the integration configuration.
func (config *IntegrationConfig) IsSecureField(path IntegrationFieldPath) bool {
f, ok := config.GetField(path)
return ok && f.Secure
}
func (config *IntegrationConfig) GetField(path IntegrationFieldPath) (IntegrationField, bool) {
for _, integrationField := range config.Fields {
if strings.EqualFold(integrationField.Name, path.Head()) {
if path.IsLeaf() {
return integrationField, true
}
return integrationField.GetField(path.Tail())
}
}
return IntegrationField{}, false
}
func (config *IntegrationConfig) GetSecretFields() []IntegrationFieldPath {
return traverseFields(config.Fields, nil, func(i IntegrationField) bool {
return i.Secure
})
}
func traverseFields(flds map[string]IntegrationField, parentPath IntegrationFieldPath, predicate func(i IntegrationField) bool) []IntegrationFieldPath {
var result []IntegrationFieldPath
for key, field := range flds {
if predicate(field) {
result = append(result, parentPath.Append(key))
}
if len(field.Fields) > 0 {
result = append(result, traverseFields(field.Fields, parentPath.Append(key), predicate)...)
}
}
return result
}
func (config *IntegrationConfig) Clone() IntegrationConfig {
clone := IntegrationConfig{
Type: config.Type,
}
if len(config.Fields) > 0 {
clone.Fields = make(map[string]IntegrationField, len(config.Fields))
for key, field := range config.Fields {
clone.Fields[key] = field.Clone()
}
}
return clone
}
func (field *IntegrationField) GetField(path IntegrationFieldPath) (IntegrationField, bool) {
for _, integrationField := range field.Fields {
if strings.EqualFold(integrationField.Name, path.Head()) {
if path.IsLeaf() {
return integrationField, true
}
return integrationField.GetField(path.Tail())
}
}
return IntegrationField{}, false
}
func (field *IntegrationField) Clone() IntegrationField {
f := IntegrationField{
Name: field.Name,
Secure: field.Secure,
Fields: make(map[string]IntegrationField, len(field.Fields)),
}
for subName, sub := range field.Fields {
f.Fields[subName] = sub.Clone()
}
return f
}
func (integration *Integration) Clone() Integration {
return Integration{
UID: integration.UID,
Name: integration.Name,
Config: integration.Config.Clone(),
DisableResolveMessage: integration.DisableResolveMessage,
Settings: cloneIntegrationSettings(integration.Settings),
SecureSettings: maps.Clone(integration.SecureSettings),
}
}
// cloneIntegrationSettings implements a deep copy of settings map.
// It's not a generic purpose function because settings are limited to basic types, maps and slices.
func cloneIntegrationSettings(m map[string]any) map[string]any {
result := maps.Clone(m) // do a shallow copy of the map first
for k, v := range result {
if mp, ok := v.(map[string]any); ok {
result[k] = cloneIntegrationSettings(mp)
continue
}
if mp, ok := v.([]any); ok {
result[k] = cloneIntegrationSettingsSlice(mp)
continue
}
}
return result
}
// cloneIntegrationSettingsSlice implements a deep copy of a []any in integration settings.
// It's not a generic purpose function because settings are limited to basic types, maps and slices.
func cloneIntegrationSettingsSlice(src []any) []any {
dst := slices.Clone(src)
for i, v := range dst {
if mp, ok := v.(map[string]any); ok {
dst[i] = cloneIntegrationSettings(mp)
continue
}
if mp, ok := v.([]any); ok {
dst[i] = cloneIntegrationSettingsSlice(mp)
continue
}
}
return dst
}
// Encrypt encrypts all fields in Settings that are marked as secure in the integration configuration. The encrypted values
// are stored in SecureSettings and the original values are removed from Settings.
// If a field is already in SecureSettings it is not encrypted again.
func (integration *Integration) Encrypt(encryptFn EncryptFn) error {
secretFieldPaths := integration.Config.GetSecretFields()
if len(secretFieldPaths) == 0 {
return nil
}
var errs []error
for _, path := range secretFieldPaths {
unencryptedSecureValue, ok, err := extractField(integration.Settings, path)
if err != nil {
errs = append(errs, fmt.Errorf("failed to extract secret field by path '%s': %w", path, err))
}
if !ok {
continue
}
if _, exists := integration.SecureSettings[path.String()]; exists {
continue
}
encrypted, err := encryptFn(unencryptedSecureValue)
if err != nil {
errs = append(errs, fmt.Errorf("failed to encrypt secure setting '%s': %w", path.String(), err))
}
integration.SecureSettings[path.String()] = encrypted
}
return errors.Join(errs...)
}
func extractField(settings map[string]any, path IntegrationFieldPath) (string, bool, error) {
val, ok := settings[path.Head()]
if !ok {
return "", false, nil
}
if path.IsLeaf() {
secret, ok := val.(string)
if !ok {
return "", false, fmt.Errorf("expected string but got %T", val)
}
delete(settings, path.Head())
return secret, true, nil
}
sub, ok := val.(map[string]any)
if !ok {
return "", false, fmt.Errorf("expected nested object but got %T", val)
}
return extractField(sub, path.Tail())
}
func getFieldValue(settings map[string]any, path IntegrationFieldPath) (any, bool) {
val, ok := settings[path.Head()]
if !ok {
return nil, false
}
if path.IsLeaf() {
return val, true
}
sub, ok := val.(map[string]any)
if !ok {
return nil, false
}
return getFieldValue(sub, path.Tail())
}
func setField(settings map[string]any, path IntegrationFieldPath, valueFn func(current any) any, skipIfNotExist bool) error {
if path.IsLeaf() {
current, ok := settings[path.Head()]
if skipIfNotExist && !ok {
return nil
}
settings[path.Head()] = valueFn(current)
return nil
}
val, ok := settings[path.Head()]
if !ok {
if skipIfNotExist {
return nil
}
val = map[string]any{}
settings[path.Head()] = val
}
sub, ok := val.(map[string]any)
if !ok {
return fmt.Errorf("expected nested object but got %T", val)
}
return setField(sub, path.Tail(), valueFn, skipIfNotExist)
}
// Decrypt decrypts all fields in SecureSettings and moves them to Settings.
// The original values are removed from SecureSettings.
func (integration *Integration) Decrypt(decryptFn DecryptFn) error {
var errs []error
for key, secureVal := range integration.SecureSettings {
decrypted, err := decryptFn(secureVal)
if err != nil {
errs = append(errs, fmt.Errorf("failed to decrypt secure setting '%s': %w", key, err))
}
delete(integration.SecureSettings, key)
path := NewIntegrationFieldPath(key)
err = setField(integration.Settings, path, func(current any) any {
return decrypted
}, false)
if err != nil {
errs = append(errs, fmt.Errorf("failed to set field '%s': %w", key, err))
}
}
return errors.Join(errs...)
}
// Redact redacts all fields in SecureSettings and moves them to Settings.
// The original values are removed from SecureSettings.
func (integration *Integration) Redact(redactFn RedactFn) {
for _, path := range integration.Config.GetSecretFields() {
_ = setField(integration.Settings, path, func(current any) any {
if s, ok := current.(string); ok && s != "" {
return redactFn(s)
}
return current
}, true)
}
for key, secureVal := range integration.SecureSettings { // TODO: Should we trust that the receiver is stored correctly or use known secure settings?
_ = setField(integration.Settings, NewIntegrationFieldPath(key), func(any) any {
return redactFn(secureVal)
}, false)
delete(integration.SecureSettings, key)
}
}
// WithExistingSecureFields copies secure settings from an existing integration. Which fields to copy is determined by the
// fields slice.
// Any fields found in Settings or SecureSettings are removed, even if they don't appear in the existing integration.
func (integration *Integration) WithExistingSecureFields(existing *Integration, fields []string) {
// Now for each field marked as secure, we copy the value from the existing receiver.
for _, secureField := range fields {
delete(integration.Settings, secureField) // Ensure secure fields are removed from new settings and secure settings.
delete(integration.SecureSettings, secureField)
if existing != nil {
if existingVal, ok := existing.SecureSettings[secureField]; ok {
integration.SecureSettings[secureField] = existingVal
}
}
}
}
// SecureFields returns a map of all secure fields in the integration. This includes fields in SecureSettings and fields
// in Settings that are marked as secure in the integration configuration.
func (integration *Integration) SecureFields() map[string]bool {
secureFields := make(map[string]bool, len(integration.SecureSettings))
if len(integration.SecureSettings) > 0 {
for key := range integration.SecureSettings {
secureFields[key] = true
}
}
// We mark secure fields in the settings as well. This is to ensure legacy behaviour for redacted secure settings.
for _, path := range integration.Config.GetSecretFields() {
if secureFields[path.String()] {
continue
}
value, ok := getFieldValue(integration.Settings, path)
if !ok || value == nil {
continue
}
if v, _ := value.(string); v != "" {
secureFields[path.String()] = true
}
}
return secureFields
}
// Validate validates the integration settings, ensuring that the integration is correctly configured.
func (integration *Integration) Validate(decryptFn DecryptFn) error {
decrypted := integration.Clone()
if err := decrypted.Decrypt(decryptFn); err != nil {
return err
}
jsonBytes, err := json.Marshal(decrypted.Settings)
if err != nil {
return err
}
return ValidateIntegration(context.Background(), alertingNotify.GrafanaIntegrationConfig{
UID: decrypted.UID,
Name: decrypted.Name,
Type: decrypted.Config.Type,
DisableResolveMessage: decrypted.DisableResolveMessage,
Settings: jsonBytes,
SecureSettings: decrypted.SecureSettings,
}, alertingNotify.NoopDecrypt)
}
func ValidateIntegration(ctx context.Context, integration alertingNotify.GrafanaIntegrationConfig, decryptFunc alertingNotify.GetDecryptedValueFn) error {
if integration.Type == "" {
return fmt.Errorf("type should not be an empty string")
}
if integration.Settings == nil {
return fmt.Errorf("settings should not be empty")
}
_, err := alertingNotify.BuildReceiverConfiguration(ctx, &alertingNotify.APIReceiver{
GrafanaIntegrations: alertingNotify.GrafanaIntegrations{
Integrations: []*alertingNotify.GrafanaIntegrationConfig{&integration},
},
}, decryptFunc)
if err != nil {
return err
}
return nil
}
type EncryptFn = func(string) (string, error)
type DecryptFn = func(string) (string, error)
type RedactFn = func(string) string
// Identified describes a class of resources that have a UID. Created to abstract required fields for authorization.
type Identified interface {
GetUID() string
}
func (r *Receiver) GetUID() string {
return r.UID
}
func (r *Receiver) Fingerprint() string {
sum := newFingerprint()
writeIntegration := func(in *Integration) {
sum.writeString(in.UID)
sum.writeString(in.Name)
// Do not include fields in fingerprint as these are not part of the receiver definition.
sum.writeString(in.Config.Type)
sum.writeBool(in.DisableResolveMessage)
// allocate a slice that will be used for sorting keys, so we allocate it only once
var keys []string
maxLen := int(math.Max(float64(len(in.Settings)), float64(len(in.SecureSettings))))
if maxLen > 0 {
keys = make([]string, maxLen)
}
writeSecureSettings := func(secureSettings map[string]string) {
// maps do not guarantee predictable sequence of keys.
// Therefore, to make hash stable, we need to sort keys
if len(secureSettings) == 0 {
return
}
idx := 0
for k := range secureSettings {
keys[idx] = k
idx++
}
sub := keys[:idx]
sort.Strings(sub)
for _, name := range sub {
sum.writeString(name)
sum.writeString(secureSettings[name])
}
}
writeSettings(sum, in.Settings)
writeSecureSettings(in.SecureSettings)
}
// fields that determine the rule state
sum.writeString(r.UID)
sum.writeString(r.Name)
sum.writeString(string(r.Provenance))
for _, integration := range r.Integrations {
writeIntegration(integration)
}
return sum.String()
}
func writeSettings(f fingerprint, m map[string]any) {
if len(m) == 0 {
f.writeBytes(nil)
return
}
keysIter := maps.Keys(m)
keys := slices.Collect(keysIter)
sort.Strings(keys)
for _, key := range keys {
f.writeString(key)
switch v := m[key].(type) {
case string:
f.writeString(v)
case bool:
f.writeBool(v)
case float64: // unmarshalling to map[string]any represents all numbers as float64
f.writeFloat64(v)
case map[string]any:
writeSettings(f, v)
default:
b, err := json.Marshal(v)
if err != nil {
f.writeString(fmt.Sprintf("%+v", v))
}
f.writeBytes(b)
}
}
}