mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
689 lines
24 KiB
Go
689 lines
24 KiB
Go
package notifier
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/exp/maps"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"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/legacy_storage"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
|
|
"github.com/grafana/grafana/pkg/services/secrets"
|
|
)
|
|
|
|
var (
|
|
ErrReceiverInUse = errutil.Conflict("alerting.notifications.receivers.used").MustTemplate(
|
|
"Receiver is used by '{{ .Public.UsedBy }}'",
|
|
errutil.WithPublic("Receiver is used by {{ .Public.UsedBy }}"),
|
|
)
|
|
ErrReceiverVersionConflict = errutil.Conflict("alerting.notifications.receivers.conflict").MustTemplate(
|
|
"Provided version '{{ .Public.Version }}' of receiver '{{ .Public.Name }}' does not match current version '{{ .Public.CurrentVersion }}'",
|
|
errutil.WithPublic("Provided version '{{ .Public.Version }}' of receiver '{{ .Public.Name }}' does not match current version '{{ .Public.CurrentVersion }}'"),
|
|
)
|
|
|
|
ErrReceiverDependentResourcesProvenance = errutil.Conflict("alerting.notifications.receivers.usedProvisioned").MustTemplate(
|
|
"Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}",
|
|
errutil.WithPublic(`Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}. You must update those resources first using the original provision method.`),
|
|
)
|
|
)
|
|
|
|
// ReceiverService is the service for managing alertmanager receivers.
|
|
type ReceiverService struct {
|
|
authz receiverAccessControlService
|
|
provisioningStore provisoningStore
|
|
cfgStore alertmanagerConfigStore
|
|
ruleNotificationsStore alertRuleNotificationSettingsStore
|
|
encryptionService secretService
|
|
xact transactionManager
|
|
log log.Logger
|
|
provenanceValidator validation.ProvenanceStatusTransitionValidator
|
|
resourcePermissions ac.ReceiverPermissionsService
|
|
}
|
|
|
|
type alertRuleNotificationSettingsStore interface {
|
|
RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver 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 secretService interface {
|
|
Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error)
|
|
Decrypt(ctx context.Context, payload []byte) ([]byte, error)
|
|
}
|
|
|
|
// receiverAccessControlService provides access control for receivers.
|
|
type receiverAccessControlService interface {
|
|
FilterRead(context.Context, identity.Requester, ...*models.Receiver) ([]*models.Receiver, error)
|
|
AuthorizeRead(context.Context, identity.Requester, *models.Receiver) error
|
|
FilterReadDecrypted(context.Context, identity.Requester, ...*models.Receiver) ([]*models.Receiver, error)
|
|
AuthorizeReadDecrypted(context.Context, identity.Requester, *models.Receiver) error
|
|
HasList(ctx context.Context, user identity.Requester) (bool, error)
|
|
|
|
AuthorizeCreate(context.Context, identity.Requester) error
|
|
AuthorizeUpdate(context.Context, identity.Requester, *models.Receiver) error
|
|
AuthorizeDeleteByUID(context.Context, identity.Requester, string) error
|
|
|
|
Access(ctx context.Context, user identity.Requester, receivers ...*models.Receiver) (map[string]models.ReceiverPermissionSet, error)
|
|
}
|
|
|
|
type alertmanagerConfigStore interface {
|
|
Get(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error)
|
|
Save(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64) error
|
|
}
|
|
|
|
type provisoningStore interface {
|
|
GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error)
|
|
GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error)
|
|
SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error
|
|
DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error
|
|
}
|
|
|
|
type transactionManager interface {
|
|
InTransaction(ctx context.Context, work func(ctx context.Context) error) error
|
|
}
|
|
|
|
func NewReceiverService(
|
|
authz receiverAccessControlService,
|
|
cfgStore alertmanagerConfigStore,
|
|
provisioningStore provisoningStore,
|
|
ruleNotificationsStore alertRuleNotificationSettingsStore,
|
|
encryptionService secretService,
|
|
xact transactionManager,
|
|
log log.Logger,
|
|
resourcePermissions ac.ReceiverPermissionsService,
|
|
) *ReceiverService {
|
|
return &ReceiverService{
|
|
authz: authz,
|
|
provisioningStore: provisioningStore,
|
|
cfgStore: cfgStore,
|
|
ruleNotificationsStore: ruleNotificationsStore,
|
|
encryptionService: encryptionService,
|
|
xact: xact,
|
|
log: log,
|
|
provenanceValidator: validation.ValidateProvenanceRelaxed,
|
|
resourcePermissions: resourcePermissions,
|
|
}
|
|
}
|
|
|
|
// GetReceiver returns a receiver by name.
|
|
// The receiver's secure settings are decrypted if requested and the user has access to do so.
|
|
func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiverQuery, user identity.Requester) (*models.Receiver, error) {
|
|
revision, err := rs.cfgStore.Get(ctx, q.OrgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
postable, err := revision.GetReceiver(legacy_storage.NameToUid(q.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rcv, err := PostableApiReceiverToReceiver(postable, getReceiverProvenance(storedProvenances, postable))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
auth := rs.authz.AuthorizeReadDecrypted
|
|
if !q.Decrypt {
|
|
auth = rs.authz.AuthorizeRead
|
|
}
|
|
if err := auth(ctx, user, rcv); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if q.Decrypt {
|
|
err := rcv.Decrypt(rs.decryptor(ctx))
|
|
if err != nil {
|
|
rs.log.Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err)
|
|
}
|
|
} else {
|
|
err := rcv.Encrypt(rs.encryptor(ctx))
|
|
if err != nil {
|
|
rs.log.Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err)
|
|
}
|
|
}
|
|
|
|
return rcv, nil
|
|
}
|
|
|
|
// GetReceivers returns a list of receivers a user has access to.
|
|
// Receivers can be filtered by name, and secure settings are decrypted if requested and the user has access to do so.
|
|
func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]*models.Receiver, error) {
|
|
uids := make([]string, 0, len(q.Names))
|
|
for _, name := range q.Names {
|
|
uids = append(uids, legacy_storage.NameToUid(name))
|
|
}
|
|
|
|
revision, err := rs.cfgStore.Get(ctx, q.OrgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
postables := revision.GetReceivers(uids)
|
|
|
|
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
receivers, err := PostableApiReceiversToReceivers(postables, storedProvenances)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filterFn := rs.authz.FilterReadDecrypted
|
|
if !q.Decrypt {
|
|
filterFn = rs.authz.FilterRead
|
|
}
|
|
filtered, err := filterFn(ctx, user, receivers...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, rcv := range filtered {
|
|
if q.Decrypt {
|
|
err := rcv.Decrypt(rs.decryptor(ctx))
|
|
if err != nil {
|
|
rs.log.Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err)
|
|
}
|
|
} else {
|
|
err := rcv.Encrypt(rs.encryptor(ctx))
|
|
if err != nil {
|
|
rs.log.Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return limitOffset(filtered, q.Offset, q.Limit), nil
|
|
}
|
|
|
|
// ListReceivers returns a list of receivers a user has access to.
|
|
// Receivers can be filtered by name.
|
|
// This offers an looser permissions compared to GetReceivers. When a user doesn't have read access it will check for list access instead of returning an empty list.
|
|
// If the users has list access, all receiver settings will be removed from the response. This option is for backwards compatibility with the v1/receivers endpoint
|
|
// and should be removed when FGAC is fully implemented.
|
|
func (rs *ReceiverService) ListReceivers(ctx context.Context, q models.ListReceiversQuery, user identity.Requester) ([]*models.Receiver, error) { // TODO: Remove this method with FGAC.
|
|
listAccess, err := rs.authz.HasList(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uids := make([]string, 0, len(q.Names))
|
|
for _, name := range q.Names {
|
|
uids = append(uids, legacy_storage.NameToUid(name))
|
|
}
|
|
|
|
revision, err := rs.cfgStore.Get(ctx, q.OrgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
postables := revision.GetReceivers(uids)
|
|
|
|
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
receivers, err := PostableApiReceiversToReceivers(postables, storedProvenances)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !listAccess {
|
|
var err error
|
|
receivers, err = rs.authz.FilterRead(ctx, user, receivers...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Remove settings.
|
|
for _, r := range receivers {
|
|
for _, integration := range r.Integrations {
|
|
integration.Settings = nil
|
|
integration.SecureSettings = nil
|
|
integration.DisableResolveMessage = false
|
|
}
|
|
}
|
|
|
|
return limitOffset(receivers, q.Offset, q.Limit), nil
|
|
}
|
|
|
|
// DeleteReceiver deletes a receiver by uid.
|
|
// UID field currently does not exist, we assume the uid is a particular hashed value of the receiver name.
|
|
func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, callerProvenance definitions.Provenance, version string, orgID int64, user identity.Requester) error {
|
|
if err := rs.authz.AuthorizeDeleteByUID(ctx, user, uid); err != nil {
|
|
return err
|
|
}
|
|
revision, err := rs.cfgStore.Get(ctx, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
postable, err := revision.GetReceiver(uid)
|
|
if err != nil {
|
|
if errors.Is(err, legacy_storage.ErrReceiverNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, orgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existing, err := PostableApiReceiverToReceiver(postable, getReceiverProvenance(storedProvenances, postable))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check optimistic concurrency.
|
|
// Optimistic concurrency is optional for delete operations, but we still check it if a version is provided.
|
|
if version != "" {
|
|
err = rs.checkOptimisticConcurrency(existing, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
rs.log.Debug("Ignoring optimistic concurrency check because version was not provided", "receiver", existing.Name, "operation", "delete")
|
|
}
|
|
|
|
if err := rs.provenanceValidator(existing.Provenance, models.Provenance(callerProvenance)); err != nil {
|
|
return err
|
|
}
|
|
|
|
usedByRoutes := revision.ReceiverNameUsedByRoutes(existing.Name)
|
|
usedByRules, err := rs.UsedByRules(ctx, orgID, existing.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if usedByRoutes || len(usedByRules) > 0 {
|
|
return makeReceiverInUseErr(usedByRoutes, usedByRules)
|
|
}
|
|
|
|
revision.DeleteReceiver(uid)
|
|
|
|
return rs.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
err = rs.cfgStore.Save(ctx, revision, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = rs.resourcePermissions.DeleteResourcePermissions(ctx, orgID, uid)
|
|
if err != nil {
|
|
rs.log.Error("Could not delete receiver permissions", "receiver", existing.Name, "error", err)
|
|
}
|
|
return rs.deleteProvenances(ctx, orgID, existing.Integrations)
|
|
})
|
|
}
|
|
|
|
func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receiver, orgID int64, user identity.Requester) (*models.Receiver, error) {
|
|
if err := rs.authz.AuthorizeCreate(ctx, user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
revision, err := rs.cfgStore.Get(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
createdReceiver := r.Clone()
|
|
err = createdReceiver.Encrypt(rs.encryptor(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := createdReceiver.Validate(rs.decryptor(ctx)); err != nil {
|
|
return nil, legacy_storage.MakeErrReceiverInvalid(err)
|
|
}
|
|
|
|
// Generate UID from name.
|
|
createdReceiver.UID = legacy_storage.NameToUid(createdReceiver.Name)
|
|
|
|
created, err := revision.CreateReceiver(&createdReceiver)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = rs.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
err = rs.cfgStore.Save(ctx, revision, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rs.resourcePermissions.SetDefaultPermissions(ctx, orgID, user, createdReceiver.GetUID())
|
|
return rs.setReceiverProvenance(ctx, orgID, &createdReceiver)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := PostableApiReceiverToReceiver(created, createdReceiver.Provenance)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receiver, storedSecureFields map[string][]string, orgID int64, user identity.Requester) (*models.Receiver, error) {
|
|
// TODO: To support receiver renaming, we need to consider permissions on old and new UID since UIDs are tied to names.
|
|
if err := rs.authz.AuthorizeUpdate(ctx, user, r); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
revision, err := rs.cfgStore.Get(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
postable, err := revision.GetReceiver(r.GetUID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, orgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
existing, err := PostableApiReceiverToReceiver(postable, getReceiverProvenance(storedProvenances, postable))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check optimistic concurrency.
|
|
err = rs.checkOptimisticConcurrency(existing, r.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := rs.provenanceValidator(existing.Provenance, r.Provenance); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We need to perform two important steps to process settings on an updated integration:
|
|
// 1. Encrypt new or updated secret fields as they will arrive in plain text.
|
|
// 2. For updates, callers do not re-send unchanged secure settings and instead mark them in SecureFields. We need
|
|
// to load these secure settings from the existing integration.
|
|
updatedReceiver := r.Clone()
|
|
err = updatedReceiver.Encrypt(rs.encryptor(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(storedSecureFields) > 0 {
|
|
updatedReceiver.WithExistingSecureFields(existing, storedSecureFields)
|
|
}
|
|
|
|
if err := updatedReceiver.Validate(rs.decryptor(ctx)); err != nil {
|
|
return nil, legacy_storage.MakeErrReceiverInvalid(err)
|
|
}
|
|
|
|
updated, err := revision.UpdateReceiver(&updatedReceiver)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = rs.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
// If the name of the receiver changed, we must update references to it in both routes and notification settings.
|
|
if existing.Name != r.Name {
|
|
err := rs.RenameReceiverInDependentResources(ctx, orgID, revision.Config.AlertmanagerConfig.Route, existing.Name, r.Name, r.Provenance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Update receiver permissions
|
|
permissionsUpdated, err := rs.resourcePermissions.CopyPermissions(ctx, orgID, user, legacy_storage.NameToUid(existing.Name), legacy_storage.NameToUid(r.Name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if permissionsUpdated > 0 {
|
|
rs.log.FromContext(ctx).Info("Moved custom receiver permissions", "oldName", existing.Name, "newName", r.Name, "count", permissionsUpdated)
|
|
}
|
|
if err := rs.resourcePermissions.DeleteResourcePermissions(ctx, orgID, legacy_storage.NameToUid(existing.Name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err = rs.cfgStore.Save(ctx, revision, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = rs.deleteProvenances(ctx, orgID, removedIntegrations(existing, &updatedReceiver))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return rs.setReceiverProvenance(ctx, orgID, &updatedReceiver)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := PostableApiReceiverToReceiver(updated, updatedReceiver.Provenance)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (rs *ReceiverService) UsedByRules(ctx context.Context, orgID int64, name string) ([]models.AlertRuleKey, error) {
|
|
keys, err := rs.ruleNotificationsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, ReceiverName: name})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return maps.Keys(keys), nil
|
|
}
|
|
|
|
// AccessControlMetadata returns access control metadata for the given Receivers.
|
|
func (rs *ReceiverService) AccessControlMetadata(ctx context.Context, user identity.Requester, receivers ...*models.Receiver) (map[string]models.ReceiverPermissionSet, error) {
|
|
return rs.authz.Access(ctx, user, receivers...)
|
|
}
|
|
|
|
// InUseMetadata returns metadata for the given Receivers about their usage in routes and rules.
|
|
func (rs *ReceiverService) InUseMetadata(ctx context.Context, orgID int64, receivers ...*models.Receiver) (map[string]models.ReceiverMetadata, error) {
|
|
revision, err := rs.cfgStore.Get(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
receiverUses := revision.ReceiverUseByName()
|
|
|
|
q := models.ListNotificationSettingsQuery{OrgID: orgID}
|
|
if len(receivers) == 1 {
|
|
q.ReceiverName = receivers[0].Name
|
|
}
|
|
keys, err := rs.ruleNotificationsStore.ListNotificationSettings(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
byReceiver := map[string][]models.AlertRuleKey{}
|
|
for key, settings := range keys {
|
|
for _, s := range settings {
|
|
if s.Receiver != "" {
|
|
byReceiver[s.Receiver] = append(byReceiver[s.Receiver], key)
|
|
}
|
|
}
|
|
}
|
|
|
|
results := make(map[string]models.ReceiverMetadata, len(receivers))
|
|
for _, rcv := range receivers {
|
|
results[rcv.GetUID()] = models.ReceiverMetadata{
|
|
InUseByRoutes: receiverUses[rcv.Name],
|
|
InUseByRules: byReceiver[rcv.Name],
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func removedIntegrations(old, new *models.Receiver) []*models.Integration {
|
|
updatedUIDs := make(map[string]struct{}, len(new.Integrations))
|
|
for _, integration := range new.Integrations {
|
|
updatedUIDs[integration.UID] = struct{}{}
|
|
}
|
|
removed := make([]*models.Integration, 0)
|
|
for _, existingIntegration := range old.Integrations {
|
|
if _, ok := updatedUIDs[existingIntegration.UID]; !ok {
|
|
removed = append(removed, existingIntegration)
|
|
}
|
|
}
|
|
return removed
|
|
}
|
|
|
|
func (rs *ReceiverService) setReceiverProvenance(ctx context.Context, orgID int64, receiver *models.Receiver) error {
|
|
// Add provenance for all integrations in the receiver.
|
|
for _, integration := range receiver.Integrations {
|
|
target := definitions.EmbeddedContactPoint{UID: integration.UID}
|
|
if err := rs.provisioningStore.SetProvenance(ctx, &target, orgID, receiver.Provenance); err != nil { // TODO: Should we set ProvenanceNone?
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (rs *ReceiverService) deleteProvenances(ctx context.Context, orgID int64, integrations []*models.Integration) error {
|
|
// Delete provenance for all integrations.
|
|
for _, integration := range integrations {
|
|
target := definitions.EmbeddedContactPoint{UID: integration.UID}
|
|
if err := rs.provisioningStore.DeleteProvenance(ctx, &target, orgID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// decryptor returns a models.DecryptFn that decrypts a secure setting. If decryption fails, the fallback value is used.
|
|
func (rs *ReceiverService) decryptor(ctx context.Context) models.DecryptFn {
|
|
return func(value string) (string, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
decrypted, err := rs.encryptionService.Decrypt(ctx, decoded)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(decrypted), nil
|
|
}
|
|
}
|
|
|
|
// encryptor creates an encrypt function that delegates to secrets.Service and returns the base64 encoded result.
|
|
func (rs *ReceiverService) encryptor(ctx context.Context) models.EncryptFn {
|
|
return func(payload string) (string, error) {
|
|
s, err := rs.encryptionService.Encrypt(ctx, []byte(payload), secrets.WithoutScope())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(s), nil
|
|
}
|
|
}
|
|
|
|
// checkOptimisticConcurrency checks if the existing receiver's version matches the desired version.
|
|
func (rs *ReceiverService) checkOptimisticConcurrency(receiver *models.Receiver, desiredVersion string) error {
|
|
if receiver.Version != desiredVersion {
|
|
return makeErrReceiverVersionConflict(receiver, desiredVersion)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// limitOffset returns a subslice of items with the given offset and limit. Returns the same underlying array, not a copy.
|
|
func limitOffset[T any](items []T, offset, limit int) []T {
|
|
if limit == 0 && offset == 0 {
|
|
return items
|
|
}
|
|
if offset >= len(items) {
|
|
return nil
|
|
}
|
|
if offset+limit >= len(items) {
|
|
return items[offset:]
|
|
}
|
|
if limit == 0 {
|
|
limit = len(items) - offset
|
|
}
|
|
return items[offset : offset+limit]
|
|
}
|
|
|
|
func makeReceiverInUseErr(usedByRoutes bool, rules []models.AlertRuleKey) error {
|
|
uids := make([]string, 0, len(rules))
|
|
for _, key := range rules {
|
|
uids = append(uids, key.UID)
|
|
}
|
|
|
|
var usedBy []string
|
|
data := make(map[string]any)
|
|
if len(uids) > 0 {
|
|
usedBy = append(usedBy, fmt.Sprintf("%d rule(s)", len(uids)))
|
|
data["UsedByRules"] = uids
|
|
}
|
|
if usedByRoutes {
|
|
usedBy = append(usedBy, "one or more routes")
|
|
data["UsedByRoutes"] = true
|
|
}
|
|
if len(usedBy) > 0 {
|
|
data["UsedBy"] = strings.Join(usedBy, ", ")
|
|
}
|
|
|
|
return ErrReceiverInUse.Build(errutil.TemplateData{
|
|
Public: data,
|
|
Error: nil,
|
|
})
|
|
}
|
|
|
|
func makeErrReceiverVersionConflict(current *models.Receiver, desiredVersion string) error {
|
|
data := errutil.TemplateData{
|
|
Public: map[string]interface{}{
|
|
"Version": desiredVersion,
|
|
"CurrentVersion": current.Version,
|
|
"Name": current.Name,
|
|
},
|
|
}
|
|
return ErrReceiverVersionConflict.Build(data)
|
|
}
|
|
|
|
func makeErrReceiverDependentResourcesProvenance(usedByRoutes bool, rules []models.AlertRuleKey) error {
|
|
uids := make([]string, 0, len(rules))
|
|
for _, key := range rules {
|
|
uids = append(uids, key.UID)
|
|
}
|
|
data := make(map[string]any, 2)
|
|
if len(uids) > 0 {
|
|
data["UsedByRules"] = uids
|
|
}
|
|
if usedByRoutes {
|
|
data["UsedByRoutes"] = true
|
|
}
|
|
|
|
return ErrReceiverDependentResourcesProvenance.Build(errutil.TemplateData{
|
|
Public: data,
|
|
})
|
|
}
|
|
|
|
func (rs *ReceiverService) RenameReceiverInDependentResources(ctx context.Context, orgID int64, route *definitions.Route, oldName, newName string, receiverProvenance models.Provenance) error {
|
|
validate := validation.ValidateProvenanceOfDependentResources(receiverProvenance)
|
|
// if there are no references to the old time interval, exit
|
|
updatedRoutes := legacy_storage.RenameReceiverInRoute(oldName, newName, route)
|
|
canUpdate := true
|
|
if updatedRoutes > 0 {
|
|
routeProvenance, err := rs.provisioningStore.GetProvenance(ctx, route, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
canUpdate = validate(routeProvenance)
|
|
}
|
|
dryRun := !canUpdate
|
|
affected, invalidProvenance, err := rs.ruleNotificationsStore.RenameReceiverInNotificationSettings(ctx, orgID, oldName, newName, validate, dryRun)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !canUpdate || len(invalidProvenance) > 0 {
|
|
return makeErrReceiverDependentResourcesProvenance(updatedRoutes > 0, invalidProvenance)
|
|
}
|
|
if len(affected) > 0 || updatedRoutes > 0 {
|
|
rs.log.FromContext(ctx).Info("Updated rules and routes that use renamed receiver", "oldName", oldName, "newName", newName, "rules", len(affected), "routes", updatedRoutes)
|
|
}
|
|
return nil
|
|
}
|