2022-04-13 15:15:55 -05:00
|
|
|
package provisioning
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
|
|
apimodels "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/store"
|
|
|
|
"github.com/grafana/grafana/pkg/services/secrets"
|
|
|
|
"github.com/grafana/grafana/pkg/util"
|
|
|
|
"github.com/prometheus/alertmanager/config"
|
|
|
|
)
|
|
|
|
|
|
|
|
type ContactPointService struct {
|
|
|
|
amStore AMConfigStore
|
|
|
|
encryptionService secrets.Service
|
|
|
|
provenanceStore ProvisioningStore
|
|
|
|
xact TransactionManager
|
|
|
|
log log.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewContactPointService(store store.AlertingStore, encryptionService secrets.Service,
|
|
|
|
provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger) *ContactPointService {
|
|
|
|
return &ContactPointService{
|
|
|
|
amStore: store,
|
|
|
|
encryptionService: encryptionService,
|
|
|
|
provenanceStore: provenanceStore,
|
|
|
|
xact: xact,
|
|
|
|
log: log,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ecp *ContactPointService) GetContactPoints(ctx context.Context, orgID int64) ([]apimodels.EmbeddedContactPoint, error) {
|
|
|
|
cfg, _, err := ecp.getCurrentConfig(ctx, orgID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
provenances, err := ecp.provenanceStore.GetProvenances(ctx, orgID, "contactPoint")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
contactPoints := []apimodels.EmbeddedContactPoint{}
|
|
|
|
for _, contactPoint := range cfg.GetGrafanaReceiverMap() {
|
|
|
|
embeddedContactPoint := apimodels.EmbeddedContactPoint{
|
|
|
|
UID: contactPoint.UID,
|
|
|
|
Type: contactPoint.Type,
|
|
|
|
Name: contactPoint.Name,
|
|
|
|
DisableResolveMessage: contactPoint.DisableResolveMessage,
|
|
|
|
Settings: contactPoint.Settings,
|
|
|
|
}
|
|
|
|
if val, exists := provenances[embeddedContactPoint.UID]; exists && val != "" {
|
|
|
|
embeddedContactPoint.Provenance = string(val)
|
|
|
|
}
|
|
|
|
for k, v := range contactPoint.SecureSettings {
|
|
|
|
decryptedValue, err := ecp.decryptValue(v)
|
|
|
|
if err != nil {
|
|
|
|
ecp.log.Warn("decrypting value failed", "err", err.Error())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if decryptedValue == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
embeddedContactPoint.Settings.Set(k, apimodels.RedactedValue)
|
|
|
|
}
|
|
|
|
contactPoints = append(contactPoints, embeddedContactPoint)
|
|
|
|
}
|
|
|
|
sort.SliceStable(contactPoints, func(i, j int) bool {
|
|
|
|
return contactPoints[i].Name < contactPoints[j].Name
|
|
|
|
})
|
|
|
|
return contactPoints, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// internal only
|
|
|
|
func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, orgID int64, uid string) (apimodels.EmbeddedContactPoint, error) {
|
|
|
|
cfg, _, err := ecp.getCurrentConfig(ctx, orgID)
|
|
|
|
if err != nil {
|
|
|
|
return apimodels.EmbeddedContactPoint{}, err
|
|
|
|
}
|
|
|
|
for _, receiver := range cfg.GetGrafanaReceiverMap() {
|
|
|
|
if receiver.UID != uid {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
embeddedContactPoint := apimodels.EmbeddedContactPoint{
|
|
|
|
UID: receiver.UID,
|
|
|
|
Type: receiver.Type,
|
|
|
|
Name: receiver.Name,
|
|
|
|
DisableResolveMessage: receiver.DisableResolveMessage,
|
|
|
|
Settings: receiver.Settings,
|
|
|
|
}
|
|
|
|
for k, v := range receiver.SecureSettings {
|
|
|
|
decryptedValue, err := ecp.decryptValue(v)
|
|
|
|
if err != nil {
|
|
|
|
ecp.log.Warn("decrypting value failed", "err", err.Error())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if decryptedValue == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
embeddedContactPoint.Settings.Set(k, decryptedValue)
|
|
|
|
}
|
|
|
|
return embeddedContactPoint, nil
|
|
|
|
}
|
|
|
|
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point with uid '%s' not found", uid)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID int64,
|
|
|
|
contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) (apimodels.EmbeddedContactPoint, error) {
|
|
|
|
if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil {
|
|
|
|
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point is not valid: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID)
|
|
|
|
if err != nil {
|
|
|
|
return apimodels.EmbeddedContactPoint{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
extractedSecrets, err := contactPoint.ExtractSecrets()
|
|
|
|
if err != nil {
|
|
|
|
return apimodels.EmbeddedContactPoint{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range extractedSecrets {
|
|
|
|
encryptedValue, err := ecp.encryptValue(v)
|
|
|
|
if err != nil {
|
|
|
|
return apimodels.EmbeddedContactPoint{}, err
|
|
|
|
}
|
|
|
|
extractedSecrets[k] = encryptedValue
|
|
|
|
}
|
|
|
|
|
|
|
|
contactPoint.UID = util.GenerateShortUID()
|
|
|
|
grafanaReceiver := &apimodels.PostableGrafanaReceiver{
|
|
|
|
UID: contactPoint.UID,
|
|
|
|
Name: contactPoint.Name,
|
|
|
|
Type: contactPoint.Type,
|
|
|
|
DisableResolveMessage: contactPoint.DisableResolveMessage,
|
|
|
|
Settings: contactPoint.Settings,
|
|
|
|
SecureSettings: extractedSecrets,
|
|
|
|
}
|
|
|
|
|
|
|
|
receiverFound := false
|
|
|
|
for _, receiver := range cfg.AlertmanagerConfig.Receivers {
|
|
|
|
if receiver.Name == contactPoint.Name {
|
|
|
|
receiver.PostableGrafanaReceivers.GrafanaManagedReceivers = append(receiver.PostableGrafanaReceivers.GrafanaManagedReceivers, grafanaReceiver)
|
|
|
|
receiverFound = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !receiverFound {
|
|
|
|
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, &apimodels.PostableApiReceiver{
|
|
|
|
Receiver: config.Receiver{
|
|
|
|
Name: grafanaReceiver.Name,
|
|
|
|
},
|
|
|
|
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
|
|
|
|
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{grafanaReceiver},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := json.Marshal(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return apimodels.EmbeddedContactPoint{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
|
|
err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{
|
|
|
|
AlertmanagerConfiguration: string(data),
|
|
|
|
FetchedConfigurationHash: fetchedHash,
|
|
|
|
ConfigurationVersion: "v1",
|
|
|
|
Default: false,
|
|
|
|
OrgID: orgID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-22 11:57:56 -05:00
|
|
|
adapter := ProvenanceOrgAdapter{
|
|
|
|
Inner: &contactPoint,
|
|
|
|
OrgID: orgID,
|
2022-04-13 15:15:55 -05:00
|
|
|
}
|
|
|
|
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
contactPoint.Provenance = string(provenance)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return apimodels.EmbeddedContactPoint{}, err
|
|
|
|
}
|
|
|
|
for k := range extractedSecrets {
|
|
|
|
contactPoint.Settings.Set(k, apimodels.RedactedValue)
|
|
|
|
}
|
|
|
|
return contactPoint, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) error {
|
|
|
|
// set all redacted values with the latest known value from the store
|
|
|
|
rawContactPoint, err := ecp.getContactPointDecrypted(ctx, orgID, contactPoint.UID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
secretKeys, err := contactPoint.SecretKeys()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, secretKey := range secretKeys {
|
|
|
|
secretValue := contactPoint.Settings.Get(secretKey).MustString()
|
|
|
|
if secretValue == apimodels.RedactedValue {
|
|
|
|
contactPoint.Settings.Set(secretKey, rawContactPoint.Settings.Get(secretKey).MustString())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// validate merged values
|
|
|
|
if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// check that provenance is not changed in a invalid way
|
2022-04-22 11:57:56 -05:00
|
|
|
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, ProvenanceOrgAdapter{
|
|
|
|
Inner: &contactPoint,
|
|
|
|
OrgID: orgID,
|
2022-04-13 15:15:55 -05:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
|
|
|
return fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance)
|
|
|
|
}
|
|
|
|
// transform to internal model
|
|
|
|
extractedSecrets, err := contactPoint.ExtractSecrets()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for k, v := range extractedSecrets {
|
|
|
|
encryptedValue, err := ecp.encryptValue(v)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
extractedSecrets[k] = encryptedValue
|
|
|
|
}
|
|
|
|
mergedReceiver := &apimodels.PostableGrafanaReceiver{
|
|
|
|
UID: contactPoint.UID,
|
|
|
|
Name: contactPoint.Name,
|
|
|
|
Type: contactPoint.Type,
|
|
|
|
DisableResolveMessage: contactPoint.DisableResolveMessage,
|
|
|
|
Settings: contactPoint.Settings,
|
|
|
|
SecureSettings: extractedSecrets,
|
|
|
|
}
|
|
|
|
// save to store
|
|
|
|
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, receiver := range cfg.AlertmanagerConfig.Receivers {
|
|
|
|
if receiver.Name == contactPoint.Name {
|
|
|
|
receiverNotFound := true
|
|
|
|
for i, grafanaReceiver := range receiver.GrafanaManagedReceivers {
|
|
|
|
if grafanaReceiver.UID == mergedReceiver.UID {
|
|
|
|
receiverNotFound = false
|
|
|
|
receiver.GrafanaManagedReceivers[i] = mergedReceiver
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if receiverNotFound {
|
|
|
|
return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
data, err := json.Marshal(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
|
|
err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{
|
|
|
|
AlertmanagerConfiguration: string(data),
|
|
|
|
FetchedConfigurationHash: fetchedHash,
|
|
|
|
ConfigurationVersion: "v1",
|
|
|
|
Default: false,
|
|
|
|
OrgID: orgID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-22 11:57:56 -05:00
|
|
|
adapter := ProvenanceOrgAdapter{
|
|
|
|
Inner: &contactPoint,
|
|
|
|
OrgID: orgID,
|
2022-04-13 15:15:55 -05:00
|
|
|
}
|
|
|
|
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
contactPoint.Provenance = string(provenance)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID int64, uid string) error {
|
|
|
|
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// Indicates if the full contact point is removed or just one of the
|
|
|
|
// configurations, as a contactpoint can consist of any number of
|
|
|
|
// configurations.
|
|
|
|
fullRemoval := false
|
|
|
|
// Name of the contact point that will be removed, might be used if a
|
|
|
|
// full removal is done to check if it's referenced in any route.
|
|
|
|
name := ""
|
|
|
|
for i, receiver := range cfg.AlertmanagerConfig.Receivers {
|
|
|
|
for j, grafanaReceiver := range receiver.GrafanaManagedReceivers {
|
|
|
|
if grafanaReceiver.UID == uid {
|
|
|
|
name = grafanaReceiver.Name
|
|
|
|
receiver.GrafanaManagedReceivers = append(receiver.GrafanaManagedReceivers[:j], receiver.GrafanaManagedReceivers[j+1:]...)
|
|
|
|
// if this was the last receiver we removed, we remove the whole receiver
|
|
|
|
if len(receiver.GrafanaManagedReceivers) == 0 {
|
|
|
|
fullRemoval = true
|
|
|
|
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:i], cfg.AlertmanagerConfig.Receivers[i+1:]...)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if fullRemoval && isContactPointInUse(name, []*apimodels.Route{cfg.AlertmanagerConfig.Route}) {
|
|
|
|
return fmt.Errorf("contact point '%s' is currently used by a notification policy", name)
|
|
|
|
}
|
|
|
|
data, err := json.Marshal(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
|
2022-04-22 11:57:56 -05:00
|
|
|
err := ecp.provenanceStore.DeleteProvenance(ctx, ProvenanceOrgAdapter{
|
|
|
|
Inner: &apimodels.EmbeddedContactPoint{
|
2022-04-13 15:15:55 -05:00
|
|
|
UID: uid,
|
|
|
|
},
|
2022-04-22 11:57:56 -05:00
|
|
|
OrgID: orgID,
|
2022-04-13 15:15:55 -05:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{
|
|
|
|
AlertmanagerConfiguration: string(data),
|
|
|
|
FetchedConfigurationHash: fetchedHash,
|
|
|
|
ConfigurationVersion: "v1",
|
|
|
|
Default: false,
|
|
|
|
OrgID: orgID,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ecp *ContactPointService) getCurrentConfig(ctx context.Context, orgID int64) (*apimodels.PostableUserConfig, string, error) {
|
|
|
|
query := &models.GetLatestAlertmanagerConfigurationQuery{
|
|
|
|
OrgID: orgID,
|
|
|
|
}
|
|
|
|
err := ecp.amStore.GetLatestAlertmanagerConfiguration(ctx, query)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
cfg, err := DeserializeAlertmanagerConfig([]byte(query.Result.AlertmanagerConfiguration))
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
return cfg, query.Result.ConfigurationHash, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func isContactPointInUse(name string, routes []*apimodels.Route) bool {
|
|
|
|
if len(routes) == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, route := range routes {
|
|
|
|
if route.Receiver == name {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if isContactPointInUse(name, route.Routes) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ecp *ContactPointService) decryptValue(value string) (string, error) {
|
|
|
|
decodeValue, err := base64.StdEncoding.DecodeString(value)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
decryptedValue, err := ecp.encryptionService.Decrypt(context.Background(), decodeValue)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(decryptedValue), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ecp *ContactPointService) encryptValue(value string) (string, error) {
|
|
|
|
encryptedData, err := ecp.encryptionService.Encrypt(context.Background(), []byte(value), secrets.WithoutScope())
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("failed to encrypt secure settings: %w", err)
|
|
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(encryptedData), nil
|
|
|
|
}
|