Alerting: Introduce initial common receiver service (#81211)

* Create locking config store that mimics existing provisioning store

* Rename existing receivers(_test).go

* Introduce shared receiver group service

* Fix test

* Move query model to models package

* ReceiverGroup -> Receiver

* Remove locking config store

* Move convert methods to compat.go

* Cleanup
This commit is contained in:
William Wernert
2024-02-01 14:42:59 -05:00
committed by GitHub
parent 80ef96e213
commit 7e939401dc
13 changed files with 577 additions and 58 deletions
@@ -1614,10 +1614,11 @@ func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) ProvisioningSrv {
t.Helper()
receiverSvc := notifier.NewReceiverService(env.ac, env.configs, env.prov, env.secrets, env.xact, env.log)
return ProvisioningSrv{
log: env.log,
policies: newFakeNotificationPolicyService(),
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, env.log, env.ac),
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log),
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log),
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, env.log),
+10
View File
@@ -0,0 +1,10 @@
package models
// GetReceiversQuery represents a query for receiver groups.
type GetReceiversQuery struct {
OrgID int64
Names []string
Limit int
Offset int
Decrypt bool
}
+3 -1
View File
@@ -313,9 +313,11 @@ func (ng *AlertNG) init() error {
ng.stateManager = stateManager
ng.schedule = scheduler
receiverService := notifier.NewReceiverService(ng.accesscontrol, ng.store, ng.store, ng.SecretsService, ng.store, ng.Log)
// Provisioning
policyService := provisioning.NewNotificationPolicyService(ng.store, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log)
contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, ng.Log, ng.accesscontrol)
contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log)
templateService := provisioning.NewTemplateService(ng.store, ng.store, ng.store, ng.Log)
muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log)
alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.dashboardService, ng.QuotaService, ng.store,
+68
View File
@@ -4,8 +4,11 @@ import (
"encoding/json"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/prometheus/alertmanager/config"
"github.com/grafana/grafana/pkg/components/simplejson"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
func PostableGrafanaReceiverToGrafanaIntegrationConfig(p *apimodels.PostableGrafanaReceiver) *alertingNotify.GrafanaIntegrationConfig {
@@ -40,3 +43,68 @@ func PostableApiAlertingConfigToApiReceivers(c apimodels.PostableApiAlertingConf
}
return apiReceivers
}
type DecryptFn = func(value string) string
func PostableToGettableGrafanaReceiver(r *apimodels.PostableGrafanaReceiver, provenance *models.Provenance, decryptFn DecryptFn) (apimodels.GettableGrafanaReceiver, error) {
out := apimodels.GettableGrafanaReceiver{
UID: r.UID,
Name: r.Name,
Type: r.Type,
DisableResolveMessage: r.DisableResolveMessage,
SecureFields: make(map[string]bool, len(r.SecureSettings)),
}
if provenance != nil {
out.Provenance = apimodels.Provenance(*provenance)
}
settings, err := simplejson.NewJson([]byte(r.Settings))
if err != nil {
return apimodels.GettableGrafanaReceiver{}, err
}
for k, v := range r.SecureSettings {
decryptedValue := decryptFn(v)
if err != nil {
return apimodels.GettableGrafanaReceiver{}, err
}
if decryptedValue == "" {
continue
} else {
settings.Set(k, decryptedValue)
}
out.SecureFields[k] = true
}
jsonBytes, err := settings.MarshalJSON()
if err != nil {
return apimodels.GettableGrafanaReceiver{}, err
}
out.Settings = jsonBytes
return out, nil
}
func PostableToGettableApiReceiver(r *apimodels.PostableApiReceiver, provenances map[string]models.Provenance, decryptFn DecryptFn) (apimodels.GettableApiReceiver, error) {
out := apimodels.GettableApiReceiver{
Receiver: config.Receiver{
Name: r.Receiver.Name,
},
}
for _, gr := range r.GrafanaManagedReceivers {
var prov *models.Provenance
if p, ok := provenances[gr.UID]; ok {
prov = &p
}
gettable, err := PostableToGettableGrafanaReceiver(gr, prov, decryptFn)
if err != nil {
return apimodels.GettableApiReceiver{}, err
}
out.GrafanaManagedReceivers = append(out.GrafanaManagedReceivers, &gettable)
}
return out, nil
}
@@ -0,0 +1,149 @@
package notifier
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"slices"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/secrets"
)
var (
// ErrPermissionDenied is returned when the user does not have permission to perform the requested action.
ErrPermissionDenied = errors.New("permission denied") // TODO: convert to errutil
)
// ReceiverService is the service for managing alertmanager receivers.
type ReceiverService struct {
ac accesscontrol.AccessControl
provisioningStore provisoningStore
cfgStore configStore
encryptionService secrets.Service
xact transactionManager
log log.Logger
}
type configStore interface {
GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error)
UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error
}
type provisoningStore interface {
GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error)
}
type transactionManager interface {
InTransaction(ctx context.Context, work func(ctx context.Context) error) error
}
func NewReceiverService(
ac accesscontrol.AccessControl,
cfgStore configStore,
provisioningStore provisoningStore,
encryptionService secrets.Service,
xact transactionManager,
log log.Logger,
) *ReceiverService {
return &ReceiverService{
ac: ac,
provisioningStore: provisioningStore,
cfgStore: cfgStore,
encryptionService: encryptionService,
xact: xact,
log: log,
}
}
func (rs *ReceiverService) canDecrypt(ctx context.Context, user identity.Requester, name string) (bool, error) {
receiverAccess := false // TODO: stub, check for read secrets access
eval := accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets)
provisioningAccess, err := rs.ac.Evaluate(ctx, user, eval)
if err != nil {
return false, err
}
return receiverAccess || provisioningAccess, nil
}
// GetReceivers returns a list of receivers a user has access to.
// Receivers can be filtered by name, and secure settings are decrypted if requested and the user has access to do so.
func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]definitions.GettableApiReceiver, error) {
baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, q.OrgID)
if err != nil {
return nil, err
}
cfg := definitions.PostableUserConfig{}
err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg)
if err != nil {
return nil, err
}
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, "contactPoint")
if err != nil {
return nil, err
}
// TODO: check for list access
var output []definitions.GettableApiReceiver
for i := q.Offset; i < len(cfg.AlertmanagerConfig.Receivers); i++ {
r := cfg.AlertmanagerConfig.Receivers[i]
if len(q.Names) > 0 && !slices.Contains(q.Names, r.Name) {
continue
}
// TODO: check for scoped read access and continue if not allowed
decryptAccess, err := rs.canDecrypt(ctx, user, r.Name)
if err != nil {
return nil, err
}
if q.Decrypt && !decryptAccess {
return nil, ErrPermissionDenied
}
decryptFn := rs.decryptOrRedact(ctx, decryptAccess && q.Decrypt, r.Name, "")
res, err := PostableToGettableApiReceiver(r, provenances, decryptFn)
if err != nil {
return nil, err
}
// TODO: redact settings if the user only has list access
output = append(output, res)
// stop if we have reached the limit or we have found all the requested receivers
if (len(output) == q.Limit && q.Limit > 0) || (len(output) == len(q.Names)) {
break
}
}
return output, nil
}
func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, name, fallback string) func(value string) string {
return func(value string) string {
if !decrypt {
return definitions.RedactedValue
}
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
rs.log.Warn("failed to decode secure setting", "name", name, "error", err)
return fallback
}
decrypted, err := rs.encryptionService.Decrypt(ctx, decoded)
if err != nil {
rs.log.Warn("failed to decrypt secure setting", "name", name, "error", err)
return fallback
}
return string(decrypted)
}
}
@@ -0,0 +1,208 @@
package notifier
import (
"context"
"encoding/json"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"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/tests/fakes"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database"
"github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestReceiverService_GetReceivers(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
t.Run("service gets receiver groups from AM config", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), rQuery(1), nil)
require.NoError(t, err)
require.Len(t, Receivers, 2)
require.Equal(t, "grafana-default-email", Receivers[0].Name)
require.Equal(t, "slack receiver", Receivers[1].Name)
})
t.Run("service filters receiver groups by name", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), rQuery(1, "slack receiver"), nil)
require.NoError(t, err)
require.Len(t, Receivers, 1)
require.Equal(t, "slack receiver", Receivers[0].Name)
})
}
func TestReceiverService_DecryptRedact(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
ac := acimpl.ProvideAccessControl(setting.NewCfg())
t.Run("service redacts receiver groups by default", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), rQuery(1, "slack receiver"), nil)
require.NoError(t, err)
require.Len(t, Receivers, 1)
rGroup := Receivers[0]
require.Equal(t, "slack receiver", rGroup.Name)
require.Len(t, rGroup.GrafanaManagedReceivers, 1)
grafanaReceiver := rGroup.GrafanaManagedReceivers[0]
require.Equal(t, "UID2", grafanaReceiver.UID)
testedSettings, err := simplejson.NewJson([]byte(grafanaReceiver.Settings))
require.NoError(t, err)
require.Equal(t, definitions.RedactedValue, testedSettings.Get("url").MustString())
})
t.Run("service returns error when trying to decrypt with nil user", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
sut.ac = ac
q := rQuery(1)
q.Decrypt = true
_, err := sut.GetReceivers(context.Background(), q, nil)
require.ErrorIs(t, err, ErrPermissionDenied)
})
t.Run("service returns error when trying to decrypt without permission", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
sut.ac = ac
q := rQuery(1)
q.Decrypt = true
_, err := sut.GetReceivers(context.Background(), q, &user.SignedInUser{})
require.ErrorIs(t, err, ErrPermissionDenied)
})
t.Run("service decrypts receiver groups with permission", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
sut.ac = ac
q := rQuery(1, "slack receiver")
q.Decrypt = true
Receivers, err := sut.GetReceivers(context.Background(), q, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionAlertingProvisioningReadSecrets: nil},
},
})
require.NoError(t, err)
require.Len(t, Receivers, 1)
rGroup := Receivers[0]
require.Equal(t, "slack receiver", rGroup.Name)
require.Len(t, rGroup.GrafanaManagedReceivers, 1)
grafanaReceiver := rGroup.GrafanaManagedReceivers[0]
require.Equal(t, "UID2", grafanaReceiver.UID)
settings, err := simplejson.NewJson([]byte(grafanaReceiver.Settings))
require.NoError(t, err)
require.Equal(t, "secure url", settings.Get("url").MustString())
})
}
func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *ReceiverService {
cfg := createEncryptedConfig(t, encryptSvc)
store := fakes.NewFakeAlertmanagerConfigStore(cfg)
xact := newNopTransactionManager()
provisioningStore := fakes.NewFakeProvisioningStore()
return &ReceiverService{
actest.FakeAccessControl{},
provisioningStore,
store,
encryptSvc,
xact,
log.NewNopLogger(),
}
}
func createEncryptedConfig(t *testing.T, secretService secrets.Service) string {
c := &definitions.PostableUserConfig{}
err := json.Unmarshal([]byte(defaultAlertmanagerConfigJSON), c)
require.NoError(t, err)
err = EncryptReceiverConfigs(c.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
return secretService.Encrypt(ctx, payload, secrets.WithoutScope())
})
require.NoError(t, err)
bytes, err := json.Marshal(c)
require.NoError(t, err)
return string(bytes)
}
func rQuery(orgID int64, names ...string) models.GetReceiversQuery {
return models.GetReceiversQuery{
OrgID: orgID,
Names: names,
}
}
const defaultAlertmanagerConfigJSON = `
{
"template_files": null,
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email",
"group_by": [
"..."
],
"routes": [{
"receiver": "grafana-default-email",
"object_matchers": [["a", "=", "b"]]
}]
},
"templates": null,
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "UID1",
"name": "grafana-default-email",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses": "\u003cexample@email.com\u003e"
},
"secureFields": {}
}]
}, {
"name": "slack receiver",
"grafana_managed_receiver_configs": [{
"uid": "UID2",
"name": "slack receiver",
"type": "slack",
"disableResolveMessage": false,
"settings": {},
"secureSettings": {"url":"secure url"}
}]
}]
}
}
`
type NopTransactionManager struct{}
func newNopTransactionManager() *NopTransactionManager {
return &NopTransactionManager{}
}
func (n *NopTransactionManager) InTransaction(ctx context.Context, work func(ctx context.Context) error) error {
return work(context.WithValue(ctx, NopTransactionManager{}, struct{}{}))
}
@@ -45,3 +45,25 @@ func PostableGrafanaReceiverToEmbeddedContactPoint(contactPoint *definitions.Pos
}
return embeddedContactPoint, nil
}
func GettableGrafanaReceiverToEmbeddedContactPoint(r *definitions.GettableGrafanaReceiver) (definitions.EmbeddedContactPoint, error) {
settingJson, err := simplejson.NewJson(r.Settings)
if err != nil {
return definitions.EmbeddedContactPoint{}, err
}
for k := range r.SecureFields {
if settingJson.Get(k).MustString() == "" {
settingJson.Set(k, definitions.RedactedValue)
}
}
return definitions.EmbeddedContactPoint{
UID: r.UID,
Name: r.Name,
Type: r.Type,
DisableResolveMessage: r.DisableResolveMessage,
Settings: settingJson,
Provenance: string(r.Provenance),
}, nil
}
@@ -12,11 +12,12 @@ import (
"github.com/prometheus/alertmanager/config"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
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/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/util"
)
@@ -26,21 +27,25 @@ type ContactPointService struct {
encryptionService secrets.Service
provenanceStore ProvisioningStore
xact TransactionManager
receiverService receiverService
log log.Logger
ac accesscontrol.AccessControl
}
type receiverService interface {
GetReceivers(ctx context.Context, query models.GetReceiversQuery, user identity.Requester) ([]apimodels.GettableApiReceiver, error)
}
func NewContactPointService(store AMConfigStore, encryptionService secrets.Service,
provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger, ac accesscontrol.AccessControl) *ContactPointService {
provenanceStore ProvisioningStore, xact TransactionManager, receiverService receiverService, log log.Logger) *ContactPointService {
return &ContactPointService{
configStore: &alertmanagerConfigStoreImpl{
store: store,
},
receiverService: receiverService,
encryptionService: encryptionService,
provenanceStore: provenanceStore,
xact: xact,
log: log,
ac: ac,
}
}
@@ -52,48 +57,38 @@ type ContactPointQuery struct {
Decrypt bool
}
func (ecp *ContactPointService) canDecryptSecrets(ctx context.Context, u identity.Requester) bool {
if u == nil {
return false
}
permitted, err := ecp.ac.Evaluate(ctx, u, accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets))
if err != nil {
ecp.log.Error("Failed to evaluate user permissions", "error", err)
permitted = false
}
return permitted
}
// GetContactPoints returns contact points. If q.Decrypt is true and the user is an OrgAdmin, decrypted secure settings are included instead of redacted ones.
func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactPointQuery, u identity.Requester) ([]apimodels.EmbeddedContactPoint, error) {
if q.Decrypt && !ecp.canDecryptSecrets(ctx, u) {
return nil, fmt.Errorf("%w: user requires Admin role or alert.provisioning.secrets:read permission to view decrypted secure settings", ErrPermissionDenied)
receiverQuery := models.GetReceiversQuery{
OrgID: q.OrgID,
Decrypt: q.Decrypt,
}
revision, err := ecp.configStore.Get(ctx, q.OrgID)
if err != nil {
return nil, err
if q.Name != "" {
receiverQuery.Names = []string{q.Name}
}
provenances, err := ecp.provenanceStore.GetProvenances(ctx, q.OrgID, "contactPoint")
if err != nil {
return nil, err
}
var contactPoints []apimodels.EmbeddedContactPoint
for _, contactPoint := range revision.cfg.GetGrafanaReceiverMap() {
if q.Name != "" && contactPoint.Name != q.Name {
continue
res, err := ecp.receiverService.GetReceivers(ctx, receiverQuery, u)
if err != nil {
return nil, convertRecSvcErr(err)
}
grafanaReceivers := []*apimodels.GettableGrafanaReceiver{}
if q.Name != "" {
grafanaReceivers = res[0].GettableGrafanaReceivers.GrafanaManagedReceivers // we only expect one receiver group
} else {
for _, r := range res {
grafanaReceivers = append(grafanaReceivers, r.GettableGrafanaReceivers.GrafanaManagedReceivers...)
}
}
embeddedContactPoint, err := PostableGrafanaReceiverToEmbeddedContactPoint(
contactPoint,
provenances[contactPoint.UID],
ecp.decryptValueOrRedacted(q.Decrypt, contactPoint.UID),
)
var contactPoints []apimodels.EmbeddedContactPoint
for _, gr := range grafanaReceivers {
contactPoint, err := GettableGrafanaReceiverToEmbeddedContactPoint(gr)
if err != nil {
return nil, err
}
contactPoints = append(contactPoints, embeddedContactPoint)
contactPoints = append(contactPoints, contactPoint)
}
sort.SliceStable(contactPoints, func(i, j int) bool {
switch strings.Compare(contactPoints[i].Name, contactPoints[j].Name) {
case -1:
@@ -103,6 +98,7 @@ func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactP
}
return contactPoints[i].UID < contactPoints[j].UID
})
return contactPoints, nil
}
@@ -507,3 +503,23 @@ func RemoveSecretsForContactPoint(e *apimodels.EmbeddedContactPoint) (map[string
}
return s, nil
}
// handleWrappedError unwraps an error and wraps it with a new expected error type. If the error is not wrapped, it returns just the expected error.
func handleWrappedError(err error, expected error) error {
err = errors.Unwrap(err)
if err == nil {
return expected
}
return fmt.Errorf("%w: %s", expected, err.Error())
}
// convertRecSvcErr converts errors from notifier.ReceiverService to errors expected from ContactPointService.
func convertRecSvcErr(err error) error {
if errors.Is(err, notifier.ErrPermissionDenied) {
return handleWrappedError(err, ErrPermissionDenied)
}
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return ErrNoAlertmanagerConfiguration.Errorf("")
}
return err
}
@@ -248,9 +248,19 @@ func TestContactPointService(t *testing.T) {
}
func TestContactPointServiceDecryptRedact(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
ac := acimpl.ProvideAccessControl(setting.NewCfg())
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
receiverServiceWithAC := func(ecp *ContactPointService) *notifier.ReceiverService {
return notifier.NewReceiverService(
acimpl.ProvideAccessControl(setting.NewCfg()),
// Get won't use the sut's config store, so we can use a different one here.
fakes.NewFakeAlertmanagerConfigStore(createEncryptedConfig(t, secretsService)),
ecp.provenanceStore,
ecp.encryptionService,
ecp.xact,
log.NewNopLogger(),
)
}
t.Run("GetContactPoints gets redacted contact points by default", func(t *testing.T) {
sut := createContactPointServiceSut(t, secretsService)
@@ -261,9 +271,10 @@ func TestContactPointServiceDecryptRedact(t *testing.T) {
require.Equal(t, "slack receiver", cps[1].Name)
require.Equal(t, definitions.RedactedValue, cps[1].Settings.Get("url").MustString())
})
t.Run("GetContactPoints errors when Decrypt = true and user does not have permissions", func(t *testing.T) {
sut := createContactPointServiceSut(t, secretsService)
sut.ac = ac
sut.receiverService = receiverServiceWithAC(sut)
q := cpsQuery(1)
q.Decrypt = true
@@ -272,7 +283,7 @@ func TestContactPointServiceDecryptRedact(t *testing.T) {
})
t.Run("GetContactPoints errors when Decrypt = true and user is nil", func(t *testing.T) {
sut := createContactPointServiceSut(t, secretsService)
sut.ac = ac
sut.receiverService = receiverServiceWithAC(sut)
q := cpsQuery(1)
q.Decrypt = true
@@ -282,7 +293,7 @@ func TestContactPointServiceDecryptRedact(t *testing.T) {
t.Run("GetContactPoints gets decrypted contact points when Decrypt = true and user has permissions", func(t *testing.T) {
sut := createContactPointServiceSut(t, secretsService)
sut.ac = ac
sut.receiverService = receiverServiceWithAC(sut)
expectedName := "slack receiver"
q := cpsQueryWithName(1, expectedName)
@@ -333,24 +344,27 @@ func TestContactPointInUse(t *testing.T) {
func createContactPointServiceSut(t *testing.T, secretService secrets.Service) *ContactPointService {
// Encrypt secure settings.
c := &definitions.PostableUserConfig{}
err := json.Unmarshal([]byte(defaultAlertmanagerConfigJSON), c)
require.NoError(t, err)
err = notifier.EncryptReceiverConfigs(c.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
return secretService.Encrypt(ctx, payload, secrets.WithoutScope())
})
require.NoError(t, err)
cfg := createEncryptedConfig(t, secretService)
store := fakes.NewFakeAlertmanagerConfigStore(cfg)
xact := newNopTransactionManager()
provisioningStore := fakes.NewFakeProvisioningStore()
raw, err := json.Marshal(c)
require.NoError(t, err)
receiverService := notifier.NewReceiverService(
actest.FakeAccessControl{},
store,
provisioningStore,
secretService,
xact,
log.NewNopLogger(),
)
return &ContactPointService{
configStore: &alertmanagerConfigStoreImpl{store: fakes.NewFakeAlertmanagerConfigStore(string(raw))},
provenanceStore: fakes.NewFakeProvisioningStore(),
xact: newNopTransactionManager(),
configStore: &alertmanagerConfigStoreImpl{store: store},
provenanceStore: provisioningStore,
receiverService: receiverService,
xact: xact,
encryptionService: secretService,
log: log.NewNopLogger(),
ac: actest.FakeAccessControl{},
}
}
@@ -376,6 +390,19 @@ func cpsQueryWithName(orgID int64, name string) ContactPointQuery {
}
}
func createEncryptedConfig(t *testing.T, secretService secrets.Service) string {
c := &definitions.PostableUserConfig{}
err := json.Unmarshal([]byte(defaultAlertmanagerConfigJSON), c)
require.NoError(t, err)
err = notifier.EncryptReceiverConfigs(c.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
return secretService.Encrypt(ctx, payload, secrets.WithoutScope())
})
require.NoError(t, err)
bytes, err := json.Marshal(c)
require.NoError(t, err)
return string(bytes)
}
func TestStitchReceivers(t *testing.T) {
type testCase struct {
name string
+15 -1
View File
@@ -9,7 +9,12 @@ import (
)
type FakeAlertmanagerConfigStore struct {
Config models.AlertConfiguration
Config models.AlertConfiguration
// GetFn is an optional function that can be set to mock the GetLatestAlertmanagerConfiguration method
GetFn func(ctx context.Context, orgID int64) (*models.AlertConfiguration, error)
// UpdateFn is an optional function that can be set to mock the UpdateAlertmanagerConfiguration method
UpdateFn func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error
// LastSaveCommand is the last command that was passed to UpdateAlertmanagerConfiguration
LastSaveCommand *models.SaveAlertmanagerConfigurationCmd
}
@@ -26,13 +31,22 @@ func NewFakeAlertmanagerConfigStore(config string) *FakeAlertmanagerConfigStore
}
func (f *FakeAlertmanagerConfigStore) GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) {
if f.GetFn != nil {
return f.GetFn(ctx, orgID)
}
result := &f.Config
result.OrgID = orgID
result.ConfigurationHash = fmt.Sprintf("%x", md5.Sum([]byte(f.Config.AlertmanagerConfiguration)))
result.ConfigurationVersion = f.Config.ConfigurationVersion
return result, nil
}
func (f *FakeAlertmanagerConfigStore) UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error {
if f.UpdateFn != nil {
return f.UpdateFn(ctx, cmd)
}
f.Config = models.AlertConfiguration{
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
ConfigurationVersion: cmd.ConfigurationVersion,
+3 -1
View File
@@ -16,6 +16,7 @@ import (
datasourceservice "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/folder"
alertingNotifier "github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/notifications"
@@ -280,8 +281,9 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
ps.log)
receiverSvc := alertingNotifier.NewReceiverService(ps.ac, &st, st, ps.secretService, ps.SQLStore, ps.log)
contactPointService := provisioning.NewContactPointService(&st, ps.secretService,
st, ps.SQLStore, ps.log, ps.ac)
st, ps.SQLStore, receiverSvc, ps.log)
notificationPolicyService := provisioning.NewNotificationPolicyService(&st,
st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log)
mutetimingsService := provisioning.NewMuteTimingService(&st, st, &st, ps.log)