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:
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user