Alerting: Receiver API complete core implementation (#91738)

* Replace global authz abstraction with one compatible with uid scope

* Replace GettableApiReceiver with models.Receiver in receiver_svc

* GrafanaIntegrationConfig -> models.Integration

* Implement Create/Update methods

* Add optimistic concurrency to receiver API

* Add scope to ReceiversRead & ReceiversReadSecrets

migrates existing permissions to include implicit global scope

* Add receiver create, update, delete actions

* Check if receiver is used by rules before delete

* On receiver name change update in routes and notification settings

* Improve errors

* Linting

* Include read permissions are requirements for create/update/delete

* Alias ngalert/models to ngmodels to differentiate from v0alpha1 model

* Ensure integration UIDs are valid, unique, and generated if empty

* Validate integration settings on create/update

* Leverage UidToName to GetReceiver instead of GetReceivers

* Remove some unnecessary uses of simplejson

* alerting.notifications.receiver -> alerting.notifications.receivers

* validator -> provenanceValidator

* Only validate the modified receiver

stops existing invalid receivers from preventing modification of a valid
receiver.

* Improve error in Integration.Encrypt

* Remove scope from alert.notifications.receivers:create

* Add todos for receiver renaming

* Use receiverAC precondition checks in k8s api

* Linting

* Optional optimistic concurrency for delete

* make update-workspace

* More specific auth checks in k8s authorize.go

* Add debug log when delete optimistic concurrency is skipped

* Improve error message on authorizer.DecisionDeny

* Keep error for non-forbidden errutil errors
This commit is contained in:
Matthew Jacobson
2024-08-26 10:47:53 -04:00
committed by GitHub
parent 22ad1cc16f
commit 32f06c6d9c
36 changed files with 3583 additions and 548 deletions
+221
View File
@@ -1,6 +1,7 @@
package models
import (
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
@@ -10,11 +11,13 @@ import (
"time"
"github.com/go-openapi/strfmt"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana-plugin-sdk-go/data"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
alertingModels "github.com/grafana/alerting/models"
@@ -1092,6 +1095,220 @@ func (n SilenceMutators) WithEmptyId() Mutator[Silence] {
}
}
// Receivers
// CopyReceiverWith creates a deep copy of Receiver and then applies mutators to it.
func CopyReceiverWith(r Receiver, mutators ...Mutator[Receiver]) Receiver {
c := r.Clone()
for _, mutator := range mutators {
mutator(&c)
}
c.Version = c.Fingerprint()
return c
}
// ReceiverGen generates Receiver using a base and mutators.
func ReceiverGen(mutators ...Mutator[Receiver]) func() Receiver {
return func() Receiver {
name := util.GenerateShortUID()
integration := IntegrationGen(IntegrationMuts.WithName(name))()
c := Receiver{
UID: nameToUid(name),
Name: name,
Integrations: []*Integration{&integration},
Provenance: ProvenanceNone,
}
for _, mutator := range mutators {
mutator(&c)
}
c.Version = c.Fingerprint()
return c
}
}
var (
ReceiverMuts = ReceiverMutators{}
)
type ReceiverMutators struct{}
func (n ReceiverMutators) WithName(name string) Mutator[Receiver] {
return func(r *Receiver) {
r.Name = name
r.UID = nameToUid(name)
}
}
func (n ReceiverMutators) WithProvenance(provenance Provenance) Mutator[Receiver] {
return func(r *Receiver) {
r.Provenance = provenance
}
}
func (n ReceiverMutators) WithValidIntegration(integrationType string) Mutator[Receiver] {
return func(r *Receiver) {
integration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
r.Integrations = []*Integration{&integration}
}
}
func (n ReceiverMutators) WithInvalidIntegration(integrationType string) Mutator[Receiver] {
return func(r *Receiver) {
integration := IntegrationGen(IntegrationMuts.WithInvalidConfig(integrationType))()
r.Integrations = []*Integration{&integration}
}
}
func (n ReceiverMutators) WithIntegrations(integration ...Integration) Mutator[Receiver] {
return func(r *Receiver) {
integrations := make([]*Integration, len(integration))
for i, v := range integration {
clone := v.Clone()
integrations[i] = &clone
}
r.Integrations = integrations
}
}
func (n ReceiverMutators) Encrypted(fn EncryptFn) Mutator[Receiver] {
return func(r *Receiver) {
_ = r.Encrypt(fn)
}
}
func (n ReceiverMutators) Decrypted(fn DecryptFn) Mutator[Receiver] {
return func(r *Receiver) {
_ = r.Decrypt(fn)
}
}
// Integrations
// CopyIntegrationWith creates a deep copy of Integration and then applies mutators to it.
func CopyIntegrationWith(r Integration, mutators ...Mutator[Integration]) Integration {
c := r.Clone()
for _, mutator := range mutators {
mutator(&c)
}
return c
}
// IntegrationGen generates Integration using a base and mutators.
func IntegrationGen(mutators ...Mutator[Integration]) func() Integration {
return func() Integration {
name := util.GenerateShortUID()
randomIntegrationType, _ := randomMapKey(alertingNotify.AllKnownConfigsForTesting)
c := Integration{
UID: util.GenerateShortUID(),
Name: name,
DisableResolveMessage: rand.Intn(2) == 1,
Settings: make(map[string]any),
SecureSettings: make(map[string]string),
}
IntegrationMuts.WithValidConfig(randomIntegrationType)(&c)
for _, mutator := range mutators {
mutator(&c)
}
return c
}
}
var (
IntegrationMuts = IntegrationMutators{}
Base64Enrypt = func(s string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(s)), nil
}
Base64Decrypt = func(s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
return string(b), err
}
)
type IntegrationMutators struct{}
func (n IntegrationMutators) WithUID(uid string) Mutator[Integration] {
return func(s *Integration) {
s.UID = uid
}
}
func (n IntegrationMutators) WithName(name string) Mutator[Integration] {
return func(s *Integration) {
s.Name = name
}
}
func (n IntegrationMutators) WithValidConfig(integrationType string) Mutator[Integration] {
return func(c *Integration) {
config := alertingNotify.AllKnownConfigsForTesting[integrationType].GetRawNotifierConfig(c.Name)
integrationConfig, _ := IntegrationConfigFromType(integrationType)
c.Config = integrationConfig
var settings map[string]any
_ = json.Unmarshal(config.Settings, &settings)
c.Settings = settings
// Decrypt secure settings over to normal settings.
for k, v := range c.SecureSettings {
decodeValue, _ := base64.StdEncoding.DecodeString(v)
settings[k] = string(decodeValue)
}
}
}
func (n IntegrationMutators) WithInvalidConfig(integrationType string) Mutator[Integration] {
return func(c *Integration) {
integrationConfig, _ := IntegrationConfigFromType(integrationType)
c.Config = integrationConfig
c.Settings = map[string]interface{}{}
c.SecureSettings = map[string]string{}
if integrationType == "webex" {
// Webex passes validation without any settings but should fail with an unparsable URL.
c.Settings["api_url"] = "(*^$*^%!@#$*()"
}
}
}
func (n IntegrationMutators) WithSettings(settings map[string]any) Mutator[Integration] {
return func(c *Integration) {
c.Settings = maps.Clone(settings)
}
}
func (n IntegrationMutators) AddSetting(key string, val any) Mutator[Integration] {
return func(c *Integration) {
c.Settings[key] = val
}
}
func (n IntegrationMutators) WithSecureSettings(secureSettings map[string]string) Mutator[Integration] {
return func(r *Integration) {
r.SecureSettings = maps.Clone(secureSettings)
}
}
func (n IntegrationMutators) AddSecureSetting(key, val string) Mutator[Integration] {
return func(r *Integration) {
r.SecureSettings[key] = val
}
}
func randomMapKey[K comparable, V any](m map[K]V) (K, V) {
randIdx := rand.Intn(len(m))
i := 0
for key, val := range m {
if i == randIdx {
return key, val
}
i++
}
return *new(K), *new(V)
}
func ConvertToRecordingRule(rule *AlertRule) {
if rule.Record == nil {
rule.Record = &Record{}
@@ -1108,3 +1325,7 @@ func ConvertToRecordingRule(rule *AlertRule) {
rule.For = 0
rule.NotificationSettings = nil
}
func nameToUid(name string) string { // Avoid legacy_storage.NameToUid import cycle.
return base64.RawURLEncoding.EncodeToString([]byte(name))
}