Files
2025-04-10 14:42:23 +02:00

595 lines
22 KiB
Go

package provisioning
import (
"context"
"encoding/base64"
"errors"
"fmt"
"sort"
"strings"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/prometheus/alertmanager/config"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/util"
)
type AlertRuleNotificationSettingsStore interface {
RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
RenameTimeIntervalInNotificationSettings(ctx context.Context, orgID int64, oldTimeInterval, newTimeInterval string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
ListNotificationSettings(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error)
}
type ContactPointService struct {
configStore alertmanagerConfigStore
encryptionService secrets.Service
provenanceStore ProvisioningStore
notificationSettingsStore AlertRuleNotificationSettingsStore
xact TransactionManager
receiverService receiverService
log log.Logger
resourcePermissions ac.ReceiverPermissionsService
}
type receiverService interface {
GetReceivers(ctx context.Context, query models.GetReceiversQuery, user identity.Requester) ([]*models.Receiver, error)
RenameReceiverInDependentResources(ctx context.Context, orgID int64, route *apimodels.Route, oldName, newName string, receiverProvenance models.Provenance) error
}
func NewContactPointService(
store alertmanagerConfigStore,
encryptionService secrets.Service,
provenanceStore ProvisioningStore,
xact TransactionManager,
receiverService receiverService,
log log.Logger,
nsStore AlertRuleNotificationSettingsStore,
resourcePermissions ac.ReceiverPermissionsService,
) *ContactPointService {
return &ContactPointService{
configStore: store,
receiverService: receiverService,
encryptionService: encryptionService,
provenanceStore: provenanceStore,
xact: xact,
log: log,
notificationSettingsStore: nsStore,
resourcePermissions: resourcePermissions,
}
}
type ContactPointQuery struct {
// Optionally filter by name.
Name string
OrgID int64
// Optionally decrypt secure settings, requires OrgAdmin.
Decrypt bool
}
// GetContactPoints returns contact points. If q.Decrypt is true and the user is an OrgAdmin, decrypted secure settings are included instead of redacted ones.
func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactPointQuery, u identity.Requester) ([]apimodels.EmbeddedContactPoint, error) {
receiverQuery := models.GetReceiversQuery{
OrgID: q.OrgID,
Decrypt: q.Decrypt,
}
if q.Name != "" {
receiverQuery.Names = []string{q.Name}
}
res, err := ecp.receiverService.GetReceivers(ctx, receiverQuery, u)
if err != nil {
return nil, convertRecSvcErr(err)
}
if q.Name != "" && len(res) > 0 {
res = []*models.Receiver{res[0]} // we only expect one receiver group
}
contactPoints := make([]apimodels.EmbeddedContactPoint, 0, len(res))
for _, recv := range res {
for _, gr := range recv.Integrations {
if !q.Decrypt {
// Provisioning API redacts by default.
gr.Redact(func(value string) string {
return apimodels.RedactedValue
})
}
contactPoints = append(contactPoints, GrafanaIntegrationConfigToEmbeddedContactPoint(gr, recv.Provenance))
}
}
sort.SliceStable(contactPoints, func(i, j int) bool {
switch strings.Compare(contactPoints[i].Name, contactPoints[j].Name) {
case -1:
return true
case 1:
return false
}
return contactPoints[i].UID < contactPoints[j].UID
})
return contactPoints, nil
}
// getContactPointDecrypted is an internal-only function that gets full contact point info, included encrypted fields.
// nil is returned if no matching contact point exists.
func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, orgID int64, uid string) (apimodels.EmbeddedContactPoint, error) {
revision, err := ecp.configStore.Get(ctx, orgID)
if err != nil {
return apimodels.EmbeddedContactPoint{}, err
}
for _, receiver := range revision.Config.GetGrafanaReceiverMap() {
if receiver.UID != uid {
continue
}
embeddedContactPoint, err := PostableGrafanaReceiverToEmbeddedContactPoint(
receiver,
models.ProvenanceNone, // TODO should be correct provenance?
ecp.decryptValueOrRedacted(true, receiver.UID),
)
if err != nil {
return apimodels.EmbeddedContactPoint{}, err
}
return embeddedContactPoint, nil
}
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("%w: contact point with uid '%s' not found", ErrNotFound, uid)
}
func (ecp *ContactPointService) CreateContactPoint(
ctx context.Context,
orgID int64,
user identity.Requester,
contactPoint apimodels.EmbeddedContactPoint,
provenance models.Provenance,
) (apimodels.EmbeddedContactPoint, error) {
if err := ValidateContactPoint(ctx, contactPoint, ecp.encryptionService.GetDecryptedValue); err != nil {
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("%w: %s", ErrValidation, err.Error())
}
revision, err := ecp.configStore.Get(ctx, orgID)
if err != nil {
return apimodels.EmbeddedContactPoint{}, err
}
extractedSecrets, err := RemoveSecretsForContactPoint(&contactPoint)
if err != nil {
return apimodels.EmbeddedContactPoint{}, err
}
for k, v := range extractedSecrets {
encryptedValue, err := ecp.encryptValue(v)
if err != nil {
return apimodels.EmbeddedContactPoint{}, err
}
extractedSecrets[k] = encryptedValue
}
if contactPoint.UID == "" {
contactPoint.UID = util.GenerateShortUID()
} else if err := util.ValidateUID(contactPoint.UID); err != nil {
return apimodels.EmbeddedContactPoint{}, errors.Join(ErrValidation, fmt.Errorf("cannot create contact point with UID '%s': %w", contactPoint.UID, err))
}
jsonData, err := contactPoint.Settings.MarshalJSON()
if err != nil {
return apimodels.EmbeddedContactPoint{}, err
}
grafanaReceiver := &apimodels.PostableGrafanaReceiver{
UID: contactPoint.UID,
Name: contactPoint.Name,
Type: contactPoint.Type,
DisableResolveMessage: contactPoint.DisableResolveMessage,
Settings: jsonData,
SecureSettings: extractedSecrets,
}
receiverFound := false
for _, receiver := range revision.Config.AlertmanagerConfig.Receivers {
// check if uid is already used in receiver
for _, rec := range receiver.GrafanaManagedReceivers {
if grafanaReceiver.UID == rec.UID {
return apimodels.EmbeddedContactPoint{}, MakeErrContactPointUidExists(rec.UID, rec.Name)
}
}
if receiver.Name == contactPoint.Name {
receiver.GrafanaManagedReceivers = append(receiver.GrafanaManagedReceivers, grafanaReceiver)
receiverFound = true
}
}
if !receiverFound {
revision.Config.AlertmanagerConfig.Receivers = append(revision.Config.AlertmanagerConfig.Receivers, &apimodels.PostableApiReceiver{
Receiver: config.Receiver{
Name: grafanaReceiver.Name,
},
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{grafanaReceiver},
},
})
}
err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
if err := ecp.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
if !receiverFound {
// Compatibility with new receiver resource permissions.
// Since this is a new receiver, we need to set default resource permissions so that viewers and editors can see and edit it.
ecp.resourcePermissions.SetDefaultPermissions(ctx, orgID, user, legacy_storage.NameToUid(contactPoint.Name))
}
return ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance)
})
if err != nil {
return apimodels.EmbeddedContactPoint{}, err
}
for k := range extractedSecrets {
contactPoint.Settings.Set(k, apimodels.RedactedValue)
}
return contactPoint, nil
}
func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) error {
// set all redacted values with the latest known value from the store
if contactPoint.Settings == nil {
return fmt.Errorf("%w: %s", ErrValidation, "settings should not be empty")
}
rawContactPoint, err := ecp.getContactPointDecrypted(ctx, orgID, contactPoint.UID)
if err != nil {
return err
}
secretKeys, err := channels_config.GetSecretKeysForContactPointType(contactPoint.Type)
if err != nil {
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
}
for _, secretKey := range secretKeys {
secretValue := contactPoint.Settings.Get(secretKey).MustString()
if secretValue == apimodels.RedactedValue {
contactPoint.Settings.Set(secretKey, rawContactPoint.Settings.Get(secretKey).MustString())
}
}
// validate merged values
if err := ValidateContactPoint(ctx, contactPoint, ecp.encryptionService.GetDecryptedValue); err != nil {
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
}
// check that provenance is not changed in an invalid way
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, &contactPoint, orgID)
if err != nil {
return err
}
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
return fmt.Errorf("cannot change provenance from '%s' to '%s'", storedProvenance, provenance)
}
// transform to internal model
extractedSecrets, err := RemoveSecretsForContactPoint(&contactPoint)
if err != nil {
return err
}
for k, v := range extractedSecrets {
encryptedValue, err := ecp.encryptValue(v)
if err != nil {
return err
}
extractedSecrets[k] = encryptedValue
}
jsonData, err := contactPoint.Settings.MarshalJSON()
if err != nil {
return err
}
mergedReceiver := &apimodels.PostableGrafanaReceiver{
UID: contactPoint.UID,
Name: contactPoint.Name,
Type: contactPoint.Type,
DisableResolveMessage: contactPoint.DisableResolveMessage,
Settings: jsonData,
SecureSettings: extractedSecrets,
}
// save to store
revision, err := ecp.configStore.Get(ctx, orgID)
if err != nil {
return err
}
oldReceiverName, fullRemoval, newReceiverCreated := stitchReceiver(revision.Config, mergedReceiver)
if oldReceiverName == "" {
return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID)
}
err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
if mergedReceiver.Name != oldReceiverName {
if newReceiverCreated {
// Copy receiver permissions
permissionsUpdated, err := ecp.resourcePermissions.CopyPermissions(ctx, orgID, nil, legacy_storage.NameToUid(oldReceiverName), legacy_storage.NameToUid(mergedReceiver.Name))
if err != nil {
return err
}
if permissionsUpdated > 0 {
ecp.log.FromContext(ctx).Debug("Moved custom receiver permissions", "oldName", oldReceiverName, "newName", mergedReceiver.Name, "count", permissionsUpdated)
}
}
if fullRemoval {
if err := ecp.receiverService.RenameReceiverInDependentResources(ctx, orgID, revision.Config.AlertmanagerConfig.Route, oldReceiverName, mergedReceiver.Name, provenance); err != nil {
return err
}
if err := ecp.resourcePermissions.DeleteResourcePermissions(ctx, orgID, legacy_storage.NameToUid(oldReceiverName)); err != nil {
return err
}
}
}
if err := ecp.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
return ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance)
})
if err != nil {
return err
}
return nil
}
func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID int64, uid string) error {
revision, err := ecp.configStore.Get(ctx, orgID)
if err != nil {
return err
}
// Indicates if the full contact point is removed or just one of the
// configurations, as a contactpoint can consist of any number of
// configurations.
fullRemoval := false
// Name of the contact point that will be removed, might be used if a
// full removal is done to check if it's referenced in any route.
name := ""
for i, receiver := range revision.Config.AlertmanagerConfig.Receivers {
for j, grafanaReceiver := range receiver.GrafanaManagedReceivers {
if grafanaReceiver.UID == uid {
name = grafanaReceiver.Name
receiver.GrafanaManagedReceivers = append(receiver.GrafanaManagedReceivers[:j], receiver.GrafanaManagedReceivers[j+1:]...)
// if this was the last receiver we removed, we remove the whole receiver
if len(receiver.GrafanaManagedReceivers) == 0 {
fullRemoval = true
revision.Config.AlertmanagerConfig.Receivers = append(revision.Config.AlertmanagerConfig.Receivers[:i], revision.Config.AlertmanagerConfig.Receivers[i+1:]...)
}
break
}
}
}
if fullRemoval && revision.ReceiverNameUsedByRoutes(name) {
return ErrContactPointReferenced.Errorf("")
}
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
if fullRemoval {
used, err := ecp.notificationSettingsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, ReceiverName: name})
if err != nil {
return fmt.Errorf("failed to query alert rules for reference to the contact point '%s': %w", name, err)
}
if len(used) > 0 {
uids := make([]string, 0, len(used))
for key := range used {
uids = append(uids, key.UID)
}
ecp.log.Error("Cannot delete contact point because it is used in rule's notification settings", "receiverName", name, "rulesUid", strings.Join(uids, ","))
return ErrContactPointUsedInRule.Errorf("")
}
// Compatibility with new receiver resource permissions.
// We need to cleanup resource permissions.
if err := ecp.resourcePermissions.DeleteResourcePermissions(ctx, orgID, legacy_storage.NameToUid(name)); err != nil {
ecp.log.Error("Could not delete receiver permissions", "receiverName", name, "error", err)
}
}
if err := ecp.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
target := &apimodels.EmbeddedContactPoint{
UID: uid,
}
return ecp.provenanceStore.DeleteProvenance(ctx, target, orgID)
})
}
// decryptValueOrRedacted returns a function that decodes a string from Base64 and then decrypts using secrets.Service.
// If argument 'decrypt' is false, then returns definitions.RedactedValue regardless of the decrypted value.
// Otherwise, it returns the decoded and decrypted value. The function returns empty string in the case of errors, which are logged
func (ecp *ContactPointService) decryptValueOrRedacted(decrypt bool, integrationUID string) func(v string) string {
return func(value string) string {
decodeValue, err := base64.StdEncoding.DecodeString(value)
if err != nil {
ecp.log.Warn("Failed to decode secret value from Base64", "error", err.Error(), "integrationUid", integrationUID)
return ""
}
decryptedValue, err := ecp.encryptionService.Decrypt(context.Background(), decodeValue)
if err != nil {
ecp.log.Warn("Failed to decrypt secret value", "error", err.Error(), "integrationUid", integrationUID)
return ""
}
if decrypt {
return string(decryptedValue)
} else {
return apimodels.RedactedValue
}
}
}
func (ecp *ContactPointService) encryptValue(value string) (string, error) {
encryptedData, err := ecp.encryptionService.Encrypt(context.Background(), []byte(value), secrets.WithoutScope())
if err != nil {
return "", fmt.Errorf("failed to encrypt secure settings: %w", err)
}
return base64.StdEncoding.EncodeToString(encryptedData), nil
}
// stitchReceiver modifies a receiver, target, in an alertmanager configStore. It modifies the given configStore in-place.
// Returns true if the configStore was altered in any way, and false otherwise.
// If integration was moved to another group and it was the last in the previous group, the second parameter contains the name of the old group that is gone
func stitchReceiver(cfg *apimodels.PostableUserConfig, target *apimodels.PostableGrafanaReceiver) (oldReceiverName string, fullRemoval bool, newReceiverCreated bool) {
// Algorithm to fix up receivers. Receivers are very complex and depend heavily on internal consistency.
// All receivers in a given receiver group have the same name. We must maintain this across renames.
groupLoop:
for groupIdx, receiverGroup := range cfg.AlertmanagerConfig.Receivers {
// Does the current group contain the grafana receiver we're interested in?
for i, grafanaReceiver := range receiverGroup.GrafanaManagedReceivers {
if grafanaReceiver.UID == target.UID {
oldReceiverName = receiverGroup.Name
// If it's a basic field change, simply replace it. Done!
//
// NOTE:
// In a "normal" database, receiverGroup.Name should always == grafanaReceiver.Name.
// Check it regardless.
// If these values are out of sync due to some bug elsewhere in the code, let's fix it up.
// Our receiver group fixing logic below will handle it.
if grafanaReceiver.Name == target.Name && receiverGroup.Name == grafanaReceiver.Name {
receiverGroup.GrafanaManagedReceivers[i] = target
break groupLoop
}
// If we're renaming, we'll need to fix up the macro receiver group for consistency.
// Firstly, if we're the only receiver in the group, simply rename the group to match. Done!
if len(receiverGroup.GrafanaManagedReceivers) == 1 {
fullRemoval = true
}
// Otherwise, we only want to rename the receiver we are touching... NOT all of them.
// Check to see whether a different group with the name we want already exists.
for _, candidateExistingGroup := range cfg.AlertmanagerConfig.Receivers {
// If so, put our modified receiver into that group. Done!
if candidateExistingGroup.Name == target.Name {
// Drop it from the old group...
receiverGroup.GrafanaManagedReceivers = append(receiverGroup.GrafanaManagedReceivers[:i], receiverGroup.GrafanaManagedReceivers[i+1:]...)
// Add the modified receiver to the new group...
candidateExistingGroup.GrafanaManagedReceivers = append(candidateExistingGroup.GrafanaManagedReceivers, target)
// if the old receiver group turns out to be empty. Remove it.
if len(receiverGroup.GrafanaManagedReceivers) == 0 {
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:groupIdx], cfg.AlertmanagerConfig.Receivers[groupIdx+1:]...)
}
break groupLoop
}
}
newReceiverCreated = true
if fullRemoval {
// Since we're going to remove the receiver group anyways, we reuse the old group to retain order.
receiverGroup.Name = target.Name
receiverGroup.GrafanaManagedReceivers[i] = target
break groupLoop
}
// Doesn't exist? Create a new group just for the receiver.
newGroup := &apimodels.PostableApiReceiver{
Receiver: config.Receiver{
Name: target.Name,
},
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{
target,
},
},
}
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, newGroup)
// Drop it from the old spot.
receiverGroup.GrafanaManagedReceivers = append(receiverGroup.GrafanaManagedReceivers[:i], receiverGroup.GrafanaManagedReceivers[i+1:]...)
break groupLoop
}
}
}
return oldReceiverName, fullRemoval, newReceiverCreated
}
func ValidateContactPoint(ctx context.Context, e apimodels.EmbeddedContactPoint, decryptFunc alertingNotify.GetDecryptedValueFn) error {
integration, err := EmbeddedContactPointToGrafanaIntegrationConfig(e)
if err != nil {
return err
}
return models.ValidateIntegration(ctx, integration, decryptFunc)
}
// RemoveSecretsForContactPoint removes all secrets from the contact point's settings and returns them as a map. Returns error if contact point type is not known.
func RemoveSecretsForContactPoint(e *apimodels.EmbeddedContactPoint) (map[string]string, error) {
s := map[string]string{}
secretKeys, err := channels_config.GetSecretKeysForContactPointType(e.Type)
if err != nil {
return nil, err
}
for _, secretKey := range secretKeys {
secretValue, err := extractCaseInsensitive(e.Settings, secretKey)
if err != nil {
return nil, err
}
if secretValue == "" {
continue
}
s[secretKey] = secretValue
}
return s, nil
}
// extractCaseInsensitive returns the value of the specified key, preferring an exact match but accepting a case-insensitive match.
// If no key matches, the second return value is an empty string.
func extractCaseInsensitive(jsonObj *simplejson.Json, key string) (string, error) {
if key == "" {
return "", nil
}
path := strings.Split(key, ".")
getNodeCaseInsensitive := func(n *simplejson.Json, field string) (string, *simplejson.Json, error) {
// Check for an exact key match first.
if value, ok := n.CheckGet(field); ok {
return field, value, nil
}
// If no exact match is found, look for a case-insensitive match.
settingsMap, err := n.Map()
if err != nil {
return "", nil, err
}
for k := range settingsMap {
if strings.EqualFold(k, field) {
return k, n.GetPath(k), nil
}
}
return "", nil, nil
}
node := jsonObj
for idx, segment := range path {
_, value, err := getNodeCaseInsensitive(node, segment)
if err != nil {
return "", err
}
if value == nil {
return "", nil
}
if idx == len(path)-1 {
resultValue := value.MustString()
node.Del(segment)
return resultValue, nil
}
node = value
}
return "", nil
}
// convertRecSvcErr converts errors from notifier.ReceiverService to errors expected from ContactPointService.
func convertRecSvcErr(err error) error {
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return legacy_storage.ErrNoAlertmanagerConfiguration.Errorf("")
}
return err
}