The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/api/api_alertmanager_guards.go

319 lines
11 KiB

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.
// - Editors don't have permission to manage all receiver permissions by default, only admin users do. This means that
// certain combined operations (e.g. swapping the name of receivers) would require careful handling to ensure
// that the correct permissions are maintained, and considering receivers don't have a unique identifier outside
// their name, this is non-trivial.
// 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
// update receivers, while allowing operations that add or remove receivers.
delta, err := calculateReceiversDelta(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers)
if err != nil {
return err
}
if len(delta.Updated) > 0 {
return fmt.Errorf("cannot update 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{}), cmp.Transformer("", func(regexp amConfig.Regexp) any {
r, _ := regexp.MarshalYAML()
return r
})}
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()
}