Alerting: Provisioning API respects global rule quota (#52190)

* Alerting: Provisioning API respects global rule quota (#52180)

* Inject interface for quota service and create mock

* Check quota and return 403 if limit exceeded

* Implement tests for quota being exceeded

(cherry picked from commit 2d7389c34d)

* backport error check

Co-authored-by: Jean-Philippe Quémémer <jeanphilippe.quemener@grafana.com>
This commit is contained in:
Alexander Weaver
2022-07-14 10:59:04 +02:00
committed by GitHub
co-authored by Jean-Philippe Quémémer
parent 9457f67210
commit f4e7d0d261
10 changed files with 209 additions and 25 deletions
+5 -2
View File
@@ -52,7 +52,7 @@ type MuteTimingService interface {
type AlertRuleService interface {
GetAlertRule(ctx context.Context, orgID int64, ruleUID string) (alerting_models.AlertRule, alerting_models.Provenance, error)
CreateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error)
CreateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance, userID int64) (alerting_models.AlertRule, error)
UpdateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error)
DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error
GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (definitions.AlertRuleGroup, error)
@@ -254,10 +254,13 @@ func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext, UID str
}
func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definitions.AlertRule) response.Response {
createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI)
createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI, c.UserId)
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
return ErrResp(http.StatusBadRequest, err, "")
}
if errors.Is(err, alerting_models.ErrQuotaReached) {
return ErrResp(http.StatusForbidden, err, "")
}
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "")
}
@@ -15,7 +15,8 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
secrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/secrets"
secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/web"
prometheus "github.com/prometheus/alertmanager/config"
@@ -259,6 +260,20 @@ func TestProvisioningApi(t *testing.T) {
require.Equal(t, 404, response.Status())
})
t.Run("have reached the rule quota, POST returns 403", func(t *testing.T) {
env := createTestEnv(t)
quotas := provisioning.MockQuotaChecker{}
quotas.EXPECT().LimitExceeded()
env.quotas = &quotas
sut := createProvisioningSrvSutFromEnv(t, &env)
rule := createTestAlertRule("rule", 1)
rc := createTestRequestCtx()
response := sut.RoutePostAlertRule(&rc, rule)
require.Equal(t, 403, response.Status())
})
})
t.Run("alert rule groups", func(t *testing.T) {
@@ -284,9 +299,21 @@ func TestProvisioningApi(t *testing.T) {
})
}
func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
// testEnvironment binds together common dependencies for testing alerting APIs.
type testEnvironment struct {
secrets secrets.Service
log log.Logger
store store.DBstore
configs provisioning.AMConfigStore
xact provisioning.TransactionManager
quotas provisioning.QuotaChecker
prov provisioning.ProvisioningStore
}
func createTestEnv(t *testing.T) testEnvironment {
t.Helper()
secrets := secrets.NewFakeSecretsService()
secrets := secrets_fakes.NewFakeSecretsService()
log := log.NewNopLogger()
configs := &provisioning.MockAMConfigStore{}
configs.EXPECT().
@@ -298,18 +325,41 @@ func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
SQLStore: sqlStore,
BaseInterval: time.Second * 10,
}
quotas := &provisioning.MockQuotaChecker{}
quotas.EXPECT().LimitOK()
xact := &provisioning.NopTransactionManager{}
prov := &provisioning.MockProvisioningStore{}
prov.EXPECT().SaveSucceeds()
prov.EXPECT().GetReturns(models.ProvenanceNone)
return testEnvironment{
secrets: secrets,
log: log,
configs: configs,
store: store,
xact: xact,
prov: prov,
quotas: quotas,
}
}
func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
t.Helper()
env := createTestEnv(t)
return createProvisioningSrvSutFromEnv(t, &env)
}
func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) ProvisioningSrv {
t.Helper()
return ProvisioningSrv{
log: log,
log: env.log,
policies: newFakeNotificationPolicyService(),
contactPointService: provisioning.NewContactPointService(configs, secrets, prov, xact, log),
templates: provisioning.NewTemplateService(configs, prov, xact, log),
muteTimings: provisioning.NewMuteTimingService(configs, prov, xact, log),
alertRules: provisioning.NewAlertRuleService(store, prov, xact, 60, 10, log),
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, 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.quotas, env.xact, 60, 10, env.log),
}
}
+2 -6
View File
@@ -39,10 +39,6 @@ type RulerSrv struct {
ac accesscontrol.AccessControl
}
var (
errQuotaReached = errors.New("quota has been exceeded")
)
// RouteDeleteAlertRules deletes all alert rules user is authorized to access in the given namespace
// or, if non-empty, a specific group of rules in the namespace
func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext, namespaceTitle string, group string) response.Response {
@@ -453,7 +449,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
return fmt.Errorf("failed to get alert rules quota: %w", err)
}
if limitReached {
return errQuotaReached
return ngmodels.ErrQuotaReached
}
}
return nil
@@ -464,7 +460,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
return ErrResp(http.StatusNotFound, err, "failed to update rule group")
} else if errors.Is(err, ngmodels.ErrAlertRuleFailedValidation) {
return ErrResp(http.StatusBadRequest, err, "failed to update rule group")
} else if errors.Is(err, errQuotaReached) {
} else if errors.Is(err, ngmodels.ErrQuotaReached) {
return ErrResp(http.StatusForbidden, err, "")
} else if errors.Is(err, ErrAuthorization) {
return ErrResp(http.StatusUnauthorized, err, "")