2022-06-04 14:55:46 +02:00
package api
import (
2022-12-16 13:01:06 -05:00
"encoding/json"
2022-06-04 14:55:46 +02:00
"fmt"
2024-09-13 12:42:33 -04:00
"strings"
2024-02-08 13:36:09 +01:00
"time"
2022-06-04 14:55:46 +02:00
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
2022-12-16 13:01:06 -05:00
amConfig "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
2022-06-04 14:55:46 +02:00
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
}
2024-09-13 12:42:33 -04:00
if err := checkContactPoints ( currentConfig . AlertmanagerConfig . Receivers , newConfig . AlertmanagerConfig . Receivers ) ; err != nil {
2022-06-04 14:55:46 +02:00
return err
}
if err := checkMuteTimes ( currentConfig , newConfig ) ; err != nil {
return err
}
return nil
}
2024-09-17 09:49:17 -04:00
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
}
2022-06-04 14:55:46 +02:00
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 ... )
2023-02-27 17:57:15 -05:00
if ! routesEqual && currentConfig . AlertmanagerConfig . Route . Provenance != apimodels . Provenance ( ngmodels . ProvenanceNone ) {
2022-06-04 14:55:46 +02:00
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 {
2023-02-27 17:57:15 -05:00
provenance = ngmodels . Provenance ( prov )
2022-06-04 14:55:46 +02:00
}
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
}
2024-09-13 12:42:33 -04:00
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
2022-06-04 14:55:46 +02:00
for _ , postedReceiver := range newReceivers {
2024-09-13 12:42:33 -04:00
newReceiversByName [ postedReceiver . Name ] = postedReceiver
2022-06-04 14:55:46 +02:00
}
2024-09-13 12:42:33 -04:00
delta := ReceiversDelta { }
2022-06-04 14:55:46 +02:00
for _ , existingReceiver := range currReceivers {
2024-09-13 12:42:33 -04:00
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.
2022-06-04 14:55:46 +02:00
}
}
2024-09-13 12:42:33 -04:00
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
2022-06-04 14:55:46 +02:00
}
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 {
2023-02-27 17:57:15 -05:00
provenance = ngmodels . Provenance ( prov )
2022-06-04 14:55:46 +02:00
}
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 { }
2024-02-08 13:36:09 +01:00
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 ( ) ,
}
2022-06-04 14:55:46 +02:00
timesEqual := cmp . Equal ( muteTime . TimeIntervals , postedMT . TimeIntervals , options ... )
if ! timesEqual {
return fmt . Errorf ( "cannot save provisioned mute time '%s'" , muteTime . Name )
}
}
return nil
}
2024-09-13 12:42:33 -04:00
// 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 ( )
}