grafana/pkg/services/ngalert/api/api_alertmanager_guards.go
Matthew Jacobson 1ea873950b
Alerting: Reject receiver update in config API when FlagAlertingApiServer enabled (#93300)
* Reject receiver update in config API when FlagAlertingApiServer enabled
2024-09-17 16:49:17 +03:00

314 lines
11 KiB
Go

package api
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
amConfig "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util/cmputil"
)
func (srv AlertmanagerSrv) provenanceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
if err := checkRoutes(currentConfig, newConfig); err != nil {
return err
}
if err := checkTemplates(currentConfig, newConfig); err != nil {
return err
}
if err := checkContactPoints(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers); err != nil {
return err
}
if err := checkMuteTimes(currentConfig, newConfig); err != nil {
return err
}
return nil
}
func (srv AlertmanagerSrv) k8sApiServiceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
// Modifications to receivers via this API is tricky with new per-receiver RBAC. Assuming we restrict the API to only
// those users with global edit permissions, we would still need to consider the following:
// - Since the UIDs stored in the database for the purposes of per-receiver RBAC are generated based on the receiver
// name, we would need to ensure continuity of permissions when a receiver is renamed. This would, preferably,
// require detecting renames and updating the permissions UID in the database.
// - It would need to determine newly created and deleted receivers so it can populate per-receiver access control defaults.
// Neither of these are insurmountable, but considering this endpoint will be removed once FlagAlertingApiServer
// becomes GA, the complexity may not be worthwhile. To that end, for now we reject any request that attempts to
// modify receivers.
delta, err := calculateReceiversDelta(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers)
if err != nil {
return err
}
if !delta.IsEmpty() {
return fmt.Errorf("cannot modify receivers using this API while per-receiver RBAC is enabled; either disable the `alertingApiServer` feature flag or use an API that supports per-receiver RBAC (e.g. provisioning or receivers API)")
}
return nil
}
func checkRoutes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
reporter := cmputil.DiffReporter{}
options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{})}
routesEqual := cmp.Equal(currentConfig.AlertmanagerConfig.Route, newConfig.AlertmanagerConfig.Route, options...)
if !routesEqual && currentConfig.AlertmanagerConfig.Route.Provenance != apimodels.Provenance(ngmodels.ProvenanceNone) {
return fmt.Errorf("policies were provisioned and cannot be changed through the UI")
}
return nil
}
func checkTemplates(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
for name, template := range currentConfig.TemplateFiles {
provenance := ngmodels.ProvenanceNone
if prov, present := currentConfig.TemplateFileProvenances[name]; present {
provenance = ngmodels.Provenance(prov)
}
if provenance == ngmodels.ProvenanceNone {
continue // we are only interested in non none
}
found := false
for newName, newTemplate := range newConfig.TemplateFiles {
if name != newName {
continue
}
found = true
if template != newTemplate {
return fmt.Errorf("cannot save provisioned template '%s'", name)
}
break // we found the template and we can proceed
}
if !found {
return fmt.Errorf("cannot delete provisioned template '%s'", name)
}
}
return nil
}
func checkContactPoints(currReceivers []*apimodels.GettableApiReceiver, newReceivers []*apimodels.PostableApiReceiver) error {
delta, err := calculateReceiversDelta(currReceivers, newReceivers)
if err != nil {
return err
}
delta = delta.ProvisionedSubset()
if !delta.IsEmpty() {
return fmt.Errorf("cannot modify provisioned contact points: %v", delta.String())
}
return nil
}
// calculateReceiversDelta calculates the changes to receivers between the current and new configuration.
func calculateReceiversDelta(currReceivers []*apimodels.GettableApiReceiver, newReceivers []*apimodels.PostableApiReceiver) (ReceiversDelta, error) {
newReceiversByName := make(map[string]*apimodels.PostableApiReceiver) // Receiver Name -> Integration UID -> ContactPoint
for _, postedReceiver := range newReceivers {
newReceiversByName[postedReceiver.Name] = postedReceiver
}
delta := ReceiversDelta{}
for _, existingReceiver := range currReceivers {
postedReceiver, present := newReceiversByName[existingReceiver.Name]
if !present {
delta.Deleted = append(delta.Deleted, existingReceiver) // Receiver has been deleted.
continue
}
// Keep track of which new receivers existed in the old config so we can add the rest to the created list.
delete(newReceiversByName, existingReceiver.Name)
updated, err := receiverUpdated(existingReceiver, postedReceiver)
if err != nil {
return ReceiversDelta{}, err
}
if updated {
delta.Updated = append(delta.Updated, existingReceiver) // Integration has been updated.
}
}
for _, postedContactPoint := range newReceiversByName {
delta.Created = append(delta.Created, postedContactPoint) // New receiver has been added.
}
return delta, nil
}
// receiverUpdated returns true if the existing and posted receivers differ.
func receiverUpdated(existing *apimodels.GettableApiReceiver, posted *apimodels.PostableApiReceiver) (bool, error) {
newCPs := make(map[string]*apimodels.PostableGrafanaReceiver)
for _, postedContactPoint := range posted.GrafanaManagedReceivers {
newCPs[postedContactPoint.UID] = postedContactPoint
}
// Check if integrations have been modified.
for _, contactPoint := range existing.GrafanaManagedReceivers {
postedContactPoint, present := newCPs[contactPoint.UID]
if !present {
return true, nil // Integration has been removed.
}
// Keep track of which new integrations existed in the old config so we can detect if any new integrations have been added.
delete(newCPs, contactPoint.UID)
updated, err := integrationUpdated(contactPoint, postedContactPoint)
if err != nil {
return false, err
}
if updated {
return true, nil // Integration has been updated.
}
}
return len(newCPs) > 0, nil // New integrations have been added.
}
// integrationUpdated returns true if the existing and posted integrations differ.
func integrationUpdated(existing *apimodels.GettableGrafanaReceiver, posted *apimodels.PostableGrafanaReceiver) (bool, error) {
if existing.DisableResolveMessage != posted.DisableResolveMessage {
return true, nil
}
if existing.Name != posted.Name {
return true, nil
}
if existing.Type != posted.Type {
return true, nil
}
for key := range existing.SecureFields {
if value, present := posted.SecureSettings[key]; present && value != "" {
return true, nil
}
}
existingSettings := map[string]any{}
err := json.Unmarshal(existing.Settings, &existingSettings)
if err != nil {
return false, err
}
newSettings := map[string]any{}
err = json.Unmarshal(posted.Settings, &newSettings)
if err != nil {
return false, err
}
d := cmp.Diff(existingSettings, newSettings)
if len(d) > 0 {
return true, nil
}
return false, nil
}
func checkMuteTimes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
newMTs := make(map[string]amConfig.MuteTimeInterval)
for _, newMuteTime := range newConfig.AlertmanagerConfig.MuteTimeIntervals {
newMTs[newMuteTime.Name] = newMuteTime
}
for _, muteTime := range currentConfig.AlertmanagerConfig.MuteTimeIntervals {
provenance := ngmodels.ProvenanceNone
if prov, present := currentConfig.AlertmanagerConfig.MuteTimeProvenances[muteTime.Name]; present {
provenance = ngmodels.Provenance(prov)
}
if provenance == ngmodels.ProvenanceNone {
continue // we are only interested in non none
}
postedMT, present := newMTs[muteTime.Name]
if !present {
return fmt.Errorf("cannot delete provisioned mute time '%s'", muteTime.Name)
}
reporter := cmputil.DiffReporter{}
options := []cmp.Option{
cmp.Reporter(&reporter),
cmp.Comparer(func(a, b *time.Location) bool {
// Check if both are nil or both have the same string representation
return (a == nil && b == nil) || (a != nil && b != nil && a.String() == b.String())
}),
cmpopts.EquateEmpty(),
}
timesEqual := cmp.Equal(muteTime.TimeIntervals, postedMT.TimeIntervals, options...)
if !timesEqual {
return fmt.Errorf("cannot save provisioned mute time '%s'", muteTime.Name)
}
}
return nil
}
// ReceiversDelta represents the changes to receivers in the alertmanager configuration.
type ReceiversDelta struct {
Created []*apimodels.PostableApiReceiver
Updated []*apimodels.GettableApiReceiver
Deleted []*apimodels.GettableApiReceiver
}
// ProvisionedSubset returns a subset of the delta containing only integrations that were provisioned.
func (d ReceiversDelta) ProvisionedSubset() ReceiversDelta {
subset := ReceiversDelta{}
for _, cp := range d.Updated {
if hasProvisionIntegration(cp) {
subset.Updated = append(subset.Updated, cp)
}
}
for _, cp := range d.Deleted {
if hasProvisionIntegration(cp) {
subset.Deleted = append(subset.Deleted, cp)
}
}
// Don't include created integrations in the subset, as they cannot have been provisioned.
return subset
}
func hasProvisionIntegration(gettable *apimodels.GettableApiReceiver) bool {
for _, integration := range gettable.GrafanaManagedReceivers {
if integration.Provenance != apimodels.Provenance(ngmodels.ProvenanceNone) {
return true
}
}
return false
}
// IsEmpty returns true if the delta contains no changes.
func (d ReceiversDelta) IsEmpty() bool {
return len(d.Created) == 0 && len(d.Updated) == 0 && len(d.Deleted) == 0
}
// String returns a human-readable representation of the delta for error messages.
func (d ReceiversDelta) String() string {
res := strings.Builder{}
if len(d.Created) > 0 {
res.WriteString("created: ")
}
for i, cp := range d.Created {
if i > 0 {
res.WriteString(", ")
}
res.WriteString(cp.Name)
}
if len(d.Updated) > 0 {
if res.Len() > 0 {
res.WriteString(", ")
}
res.WriteString("updated: ")
}
for i, cp := range d.Updated {
if i > 0 {
res.WriteString(", ")
}
res.WriteString(cp.Name)
}
if len(d.Deleted) > 0 {
if res.Len() > 0 {
res.WriteString(", ")
}
res.WriteString("deleted: ")
}
for i, cp := range d.Deleted {
if i > 0 {
res.WriteString(", ")
}
res.WriteString(cp.Name)
}
return res.String()
}