[Alerting]: Extend quota service to optionally set limits on alerts (#33283)

* Quota: Extend service to set limit on alerts

* Add test for applying quota to alert rules

* Apply suggestions from code review

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Get used alert quota only if naglert is enabled

* Set alert limit to zero if nglalert is not enabled
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
Sofia Papagiannaki 2021-05-04 19:16:28 +03:00 committed by GitHub
parent 985331e813
commit 540f110220
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 464 additions and 89 deletions

View File

@ -651,6 +651,9 @@ org_data_source = 10
# limit number of api_keys per Org. # limit number of api_keys per Org.
org_api_key = 10 org_api_key = 10
# limit number of alerts per Org.
org_alert_rule = 100
# limit number of orgs a user can create. # limit number of orgs a user can create.
user_org = 10 user_org = 10
@ -669,6 +672,9 @@ global_api_key = -1
# global limit on number of logged in users. # global limit on number of logged in users.
global_session = -1 global_session = -1
# global limit of alerts
global_alert_rule = -1
#################################### Alerting ############################ #################################### Alerting ############################
[alerting] [alerting]
# Disable alerting engine & UI features # Disable alerting engine & UI features

View File

@ -640,6 +640,9 @@
# limit number of api_keys per Org. # limit number of api_keys per Org.
; org_api_key = 10 ; org_api_key = 10
# limit number of alerts per Org.
;org_alert_rule = 100
# limit number of orgs a user can create. # limit number of orgs a user can create.
; user_org = 10 ; user_org = 10
@ -658,6 +661,9 @@
# global limit on number of logged in users. # global limit on number of logged in users.
; global_session = -1 ; global_session = -1
# global limit of alerts
;global_alert_rule = -1
#################################### Alerting ############################ #################################### Alerting ############################
[alerting] [alerting]
# Disable alerting engine & UI features # Disable alerting engine & UI features

View File

@ -1021,6 +1021,10 @@ Limit the number of data sources allowed per organization. Default is 10.
Limit the number of API keys that can be entered per organization. Default is 10. Limit the number of API keys that can be entered per organization. Default is 10.
### org_alert_rule
Limit the number of alert rules that can be entered per organization. Default is 100.
### user_org ### user_org
Limit the number of organizations a user can create. Default is 10. Limit the number of organizations a user can create. Default is 10.
@ -1045,6 +1049,10 @@ Sets global limit of API keys that can be entered. Default is -1 (unlimited).
Sets a global limit on number of users that can be logged in at one time. Default is -1 (unlimited). Sets a global limit on number of users that can be logged in at one time. Default is -1 (unlimited).
### global_alert_rule
Sets a global limit on number of alert rules that can be created. Default is -1 (unlimited).
<hr> <hr>
## [alerting] ## [alerting]

View File

@ -208,6 +208,61 @@ func TestMiddlewareQuota(t *testing.T) {
cfg.Quota.Org.Dashboard = quotaUsed cfg.Quota.Org.Dashboard = quotaUsed
cfg.Quota.Enabled = false cfg.Quota.Enabled = false
}) })
middlewareScenario(t, "org alert quota reached and ngalert enabled", func(t *testing.T, sc *scenarioContext) {
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.FeatureToggles = map[string]bool{"ngalert": true}
cfg.Quota.Org.AlertRule = quotaUsed
})
middlewareScenario(t, "org alert quota not reached and ngalert enabled", func(t *testing.T, sc *scenarioContext) {
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.FeatureToggles = map[string]bool{"ngalert": true}
cfg.Quota.Org.AlertRule = quotaUsed + 1
})
middlewareScenario(t, "org alert quota reached but ngalert disabled", func(t *testing.T, sc *scenarioContext) {
// this scenario can only happen if the feature was enabled and later disabled
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.AlertRule = quotaUsed
})
middlewareScenario(t, "org alert quota not reached but ngalert disabled", func(t *testing.T, sc *scenarioContext) {
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.AlertRule = quotaUsed + 1
})
}) })
} }
@ -230,6 +285,7 @@ func configure(cfg *setting.Cfg) {
Dashboard: 5, Dashboard: 5,
DataSource: 5, DataSource: 5,
ApiKey: 5, ApiKey: 5,
AlertRule: 5,
}, },
User: &setting.UserQuota{ User: &setting.UserQuota{
Org: 5, Org: 5,
@ -241,6 +297,7 @@ func configure(cfg *setting.Cfg) {
DataSource: 5, DataSource: 5,
ApiKey: 5, ApiKey: 5,
Session: 5, Session: 5,
AlertRule: 5,
}, },
} }
} }

View File

@ -44,33 +44,38 @@ type GlobalQuotaDTO struct {
} }
type GetOrgQuotaByTargetQuery struct { type GetOrgQuotaByTargetQuery struct {
Target string Target string
OrgId int64 OrgId int64
Default int64 Default int64
Result *OrgQuotaDTO IsNgAlertEnabled bool
Result *OrgQuotaDTO
} }
type GetOrgQuotasQuery struct { type GetOrgQuotasQuery struct {
OrgId int64 OrgId int64
Result []*OrgQuotaDTO IsNgAlertEnabled bool
Result []*OrgQuotaDTO
} }
type GetUserQuotaByTargetQuery struct { type GetUserQuotaByTargetQuery struct {
Target string Target string
UserId int64 UserId int64
Default int64 Default int64
Result *UserQuotaDTO IsNgAlertEnabled bool
Result *UserQuotaDTO
} }
type GetUserQuotasQuery struct { type GetUserQuotasQuery struct {
UserId int64 UserId int64
Result []*UserQuotaDTO IsNgAlertEnabled bool
Result []*UserQuotaDTO
} }
type GetGlobalQuotaByTargetQuery struct { type GetGlobalQuotaByTargetQuery struct {
Target string Target string
Default int64 Default int64
Result *GlobalQuotaDTO IsNgAlertEnabled bool
Result *GlobalQuotaDTO
} }
type UpdateOrgQuotaCmd struct { type UpdateOrgQuotaCmd struct {

View File

@ -3,6 +3,8 @@ package api
import ( import (
"time" "time"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state"
@ -41,6 +43,7 @@ type API struct {
DatasourceCache datasources.CacheService DatasourceCache datasources.CacheService
RouteRegister routing.RouteRegister RouteRegister routing.RouteRegister
DataService *tsdb.Service DataService *tsdb.Service
QuotaService *quota.QuotaService
Schedule schedule.ScheduleService Schedule schedule.ScheduleService
RuleStore store.RuleStore RuleStore store.RuleStore
InstanceStore store.InstanceStore InstanceStore store.InstanceStore
@ -73,7 +76,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.Metrics) {
api.RegisterRulerApiEndpoints(NewForkedRuler( api.RegisterRulerApiEndpoints(NewForkedRuler(
api.DatasourceCache, api.DatasourceCache,
NewLotexRuler(proxy, logger), NewLotexRuler(proxy, logger),
RulerSrv{DatasourceCache: api.DatasourceCache, manager: api.StateManager, store: api.RuleStore, log: logger}, RulerSrv{DatasourceCache: api.DatasourceCache, QuotaService: api.QuotaService, manager: api.StateManager, store: api.RuleStore, log: logger},
), m) ), m)
api.RegisterTestingApiEndpoints(TestingApiSrv{ api.RegisterTestingApiEndpoints(TestingApiSrv{
AlertingProxy: proxy, AlertingProxy: proxy,

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota"
coreapi "github.com/grafana/grafana/pkg/api" coreapi "github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
@ -23,6 +24,7 @@ import (
type RulerSrv struct { type RulerSrv struct {
store store.RuleStore store store.RuleStore
DatasourceCache datasources.CacheService DatasourceCache datasources.CacheService
QuotaService *quota.QuotaService
manager *state.Manager manager *state.Manager
log log.Logger log log.Logger
} }
@ -209,8 +211,18 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
return toNamespaceErrorResponse(err) return toNamespaceErrorResponse(err)
} }
// TODO check permissions // quotas are checked in advanced
// TODO check quota // that is acceptable under the assumption that there will be only one alert rule under the rule group
// alternatively we should check the quotas after the rule group update
// and rollback the transaction in case of violation
limitReached, err := srv.QuotaService.QuotaReached(c, "alert_rule")
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to get quota", err)
}
if limitReached {
return response.Error(http.StatusForbidden, "quota reached", nil)
}
// TODO validate UID uniqueness in the payload // TODO validate UID uniqueness in the payload
//TODO: Should this belong in alerting-api? //TODO: Should this belong in alerting-api?

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"time" "time"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state"
@ -46,6 +48,7 @@ type AlertNG struct {
DataService *tsdb.Service `inject:""` DataService *tsdb.Service `inject:""`
Alertmanager *notifier.Alertmanager `inject:""` Alertmanager *notifier.Alertmanager `inject:""`
DataProxy *datasourceproxy.DatasourceProxyService `inject:""` DataProxy *datasourceproxy.DatasourceProxyService `inject:""`
QuotaService *quota.QuotaService `inject:""`
Metrics *metrics.Metrics `inject:""` Metrics *metrics.Metrics `inject:""`
Log log.Logger Log log.Logger
schedule schedule.ScheduleService schedule schedule.ScheduleService
@ -83,6 +86,7 @@ func (ng *AlertNG) Init() error {
DataService: ng.DataService, DataService: ng.DataService,
Schedule: ng.schedule, Schedule: ng.schedule,
DataProxy: ng.DataProxy, DataProxy: ng.DataProxy,
QuotaService: ng.QuotaService,
InstanceStore: store, InstanceStore: store,
RuleStore: store, RuleStore: store,
AlertingStore: store, AlertingStore: store,

View File

@ -64,7 +64,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool,
} }
continue continue
} }
query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target} query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return true, err return true, err
} }
@ -75,7 +75,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool,
if !c.IsSignedIn { if !c.IsSignedIn {
continue continue
} }
query := models.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit} query := models.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return true, err return true, err
} }
@ -93,7 +93,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool,
if !c.IsSignedIn || c.UserId == 0 { if !c.IsSignedIn || c.UserId == 0 {
continue continue
} }
query := models.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit} query := models.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return true, err return true, err
} }
@ -151,6 +151,12 @@ func (qs *QuotaService) getQuotaScopes(target string) ([]models.QuotaScope, erro
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Session}, models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Session},
) )
return scopes, nil return scopes, nil
case "alert_rule": // target need to match the respective database name
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.AlertRule},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.AlertRule},
)
return scopes, nil
default: default:
return scopes, ErrInvalidQuotaTarget return scopes, ErrInvalidQuotaTarget
} }

View File

@ -9,6 +9,8 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
const ALERT_RULE_TARGET = "alert_rule"
func init() { func init() {
bus.AddHandler("sql", GetOrgQuotaByTarget) bus.AddHandler("sql", GetOrgQuotaByTarget)
bus.AddHandler("sql", GetOrgQuotas) bus.AddHandler("sql", GetOrgQuotas)
@ -35,18 +37,22 @@ func GetOrgQuotaByTarget(query *models.GetOrgQuotaByTargetQuery) error {
quota.Limit = query.Default quota.Limit = query.Default
} }
// get quota used. var used int64
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(query.Target)) if query.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
resp := make([]*targetCount, 0) // get quota used.
if err := x.SQL(rawSQL, query.OrgId).Find(&resp); err != nil { rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(query.Target))
return err resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, query.OrgId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
} }
query.Result = &models.OrgQuotaDTO{ query.Result = &models.OrgQuotaDTO{
Target: query.Target, Target: query.Target,
Limit: quota.Limit, Limit: quota.Limit,
OrgId: query.OrgId, OrgId: query.OrgId,
Used: resp[0].Count, Used: used,
} }
return nil return nil
@ -78,17 +84,21 @@ func GetOrgQuotas(query *models.GetOrgQuotasQuery) error {
result := make([]*models.OrgQuotaDTO, len(quotas)) result := make([]*models.OrgQuotaDTO, len(quotas))
for i, q := range quotas { for i, q := range quotas {
// get quota used. var used int64
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target)) if q.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
resp := make([]*targetCount, 0) // get quota used.
if err := x.SQL(rawSQL, q.OrgId).Find(&resp); err != nil { rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target))
return err resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, q.OrgId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
} }
result[i] = &models.OrgQuotaDTO{ result[i] = &models.OrgQuotaDTO{
Target: q.Target, Target: q.Target,
Limit: q.Limit, Limit: q.Limit,
OrgId: q.OrgId, OrgId: q.OrgId,
Used: resp[0].Count, Used: used,
} }
} }
query.Result = result query.Result = result
@ -138,18 +148,22 @@ func GetUserQuotaByTarget(query *models.GetUserQuotaByTargetQuery) error {
quota.Limit = query.Default quota.Limit = query.Default
} }
// get quota used. var used int64
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target)) if query.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
resp := make([]*targetCount, 0) // get quota used.
if err := x.SQL(rawSQL, query.UserId).Find(&resp); err != nil { rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target))
return err resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, query.UserId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
} }
query.Result = &models.UserQuotaDTO{ query.Result = &models.UserQuotaDTO{
Target: query.Target, Target: query.Target,
Limit: quota.Limit, Limit: quota.Limit,
UserId: query.UserId, UserId: query.UserId,
Used: resp[0].Count, Used: used,
} }
return nil return nil
@ -181,17 +195,21 @@ func GetUserQuotas(query *models.GetUserQuotasQuery) error {
result := make([]*models.UserQuotaDTO, len(quotas)) result := make([]*models.UserQuotaDTO, len(quotas))
for i, q := range quotas { for i, q := range quotas {
// get quota used. var used int64
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target)) if q.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
resp := make([]*targetCount, 0) // get quota used.
if err := x.SQL(rawSQL, q.UserId).Find(&resp); err != nil { rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target))
return err resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, q.UserId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
} }
result[i] = &models.UserQuotaDTO{ result[i] = &models.UserQuotaDTO{
Target: q.Target, Target: q.Target,
Limit: q.Limit, Limit: q.Limit,
UserId: q.UserId, UserId: q.UserId,
Used: resp[0].Count, Used: used,
} }
} }
query.Result = result query.Result = result
@ -230,17 +248,22 @@ func UpdateUserQuota(cmd *models.UpdateUserQuotaCmd) error {
} }
func GetGlobalQuotaByTarget(query *models.GetGlobalQuotaByTargetQuery) error { func GetGlobalQuotaByTarget(query *models.GetGlobalQuotaByTargetQuery) error {
// get quota used. var used int64
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s", dialect.Quote(query.Target)) if query.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
resp := make([]*targetCount, 0) // get quota used.
if err := x.SQL(rawSQL).Find(&resp); err != nil {
return err rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL).Find(&resp); err != nil {
return err
}
used = resp[0].Count
} }
query.Result = &models.GlobalQuotaDTO{ query.Result = &models.GlobalQuotaDTO{
Target: query.Target, Target: query.Target,
Limit: query.Default, Limit: query.Default,
Used: resp[0].Count, Used: used,
} }
return nil return nil

View File

@ -24,6 +24,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
Dashboard: 5, Dashboard: 5,
DataSource: 5, DataSource: 5,
ApiKey: 5, ApiKey: 5,
AlertRule: 5,
}, },
User: &setting.UserQuota{ User: &setting.UserQuota{
Org: 5, Org: 5,
@ -35,6 +36,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
DataSource: 5, DataSource: 5,
ApiKey: 5, ApiKey: 5,
Session: 5, Session: 5,
AlertRule: 5,
}, },
} }
@ -87,12 +89,19 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Result.Used, ShouldEqual, 0) So(query.Result.Used, ShouldEqual, 0)
}) })
Convey("Should be able to get zero used org alert quota when table does not exist (ngalert is not enabled - default case)", func() {
query := models.GetOrgQuotaByTargetQuery{OrgId: 2, Target: "alert", Default: 11}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Used, ShouldEqual, 0)
})
Convey("Should be able to quota list for org", func() { Convey("Should be able to quota list for org", func() {
query := models.GetOrgQuotasQuery{OrgId: orgId} query := models.GetOrgQuotasQuery{OrgId: orgId}
err = GetOrgQuotas(&query) err = GetOrgQuotas(&query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 4) So(len(query.Result), ShouldEqual, 5)
for _, res := range query.Result { for _, res := range query.Result {
limit := 5 // default quota limit limit := 5 // default quota limit
used := 0 used := 0
@ -169,6 +178,14 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
So(query.Result.Limit, ShouldEqual, 5) So(query.Result.Limit, ShouldEqual, 5)
So(query.Result.Used, ShouldEqual, 1) So(query.Result.Used, ShouldEqual, 1)
}) })
Convey("Should be able to get zero used global alert quota when table does not exist (ngalert is not enabled - default case)", func() {
query := models.GetGlobalQuotaByTargetQuery{Target: "alert_rule", Default: 5}
err = GetGlobalQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
So(query.Result.Used, ShouldEqual, 0)
})
// related: https://github.com/grafana/grafana/issues/14342 // related: https://github.com/grafana/grafana/issues/14342
Convey("Should org quota updating is successful even if it called multiple time", func() { Convey("Should org quota updating is successful even if it called multiple time", func() {

View File

@ -158,7 +158,6 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser() error {
// ensure admin user // ensure admin user
if !ss.Cfg.DisableInitAdminCreation { if !ss.Cfg.DisableInitAdminCreation {
ss.log.Debug("Creating default admin user")
ss.log.Debug("Creating default admin user") ss.log.Debug("Creating default admin user")
if _, err := ss.createUser(ctx, sess, userCreationArgs{ if _, err := ss.createUser(ctx, sess, userCreationArgs{
Login: ss.Cfg.AdminUser, Login: ss.Cfg.AdminUser,

View File

@ -9,6 +9,7 @@ type OrgQuota struct {
DataSource int64 `target:"data_source"` DataSource int64 `target:"data_source"`
Dashboard int64 `target:"dashboard"` Dashboard int64 `target:"dashboard"`
ApiKey int64 `target:"api_key"` ApiKey int64 `target:"api_key"`
AlertRule int64 `target:"alert_rule"`
} }
type UserQuota struct { type UserQuota struct {
@ -22,6 +23,7 @@ type GlobalQuota struct {
Dashboard int64 `target:"dashboard"` Dashboard int64 `target:"dashboard"`
ApiKey int64 `target:"api_key"` ApiKey int64 `target:"api_key"`
Session int64 `target:"-"` Session int64 `target:"-"`
AlertRule int64 `target:"alert_rule"`
} }
func (q *OrgQuota) ToMap() map[string]int64 { func (q *OrgQuota) ToMap() map[string]int64 {
@ -64,12 +66,19 @@ func (cfg *Cfg) readQuotaSettings() {
quota := cfg.Raw.Section("quota") quota := cfg.Raw.Section("quota")
Quota.Enabled = quota.Key("enabled").MustBool(false) Quota.Enabled = quota.Key("enabled").MustBool(false)
var alertOrgQuota int64
var alertGlobalQuota int64
if cfg.IsNgAlertEnabled() {
alertOrgQuota = quota.Key("org_alert_rule").MustInt64(100)
alertGlobalQuota = quota.Key("global_alert_rule").MustInt64(-1)
}
// per ORG Limits // per ORG Limits
Quota.Org = &OrgQuota{ Quota.Org = &OrgQuota{
User: quota.Key("org_user").MustInt64(10), User: quota.Key("org_user").MustInt64(10),
DataSource: quota.Key("org_data_source").MustInt64(10), DataSource: quota.Key("org_data_source").MustInt64(10),
Dashboard: quota.Key("org_dashboard").MustInt64(10), Dashboard: quota.Key("org_dashboard").MustInt64(10),
ApiKey: quota.Key("org_api_key").MustInt64(10), ApiKey: quota.Key("org_api_key").MustInt64(10),
AlertRule: alertOrgQuota,
} }
// per User limits // per User limits
@ -85,6 +94,7 @@ func (cfg *Cfg) readQuotaSettings() {
Dashboard: quota.Key("global_dashboard").MustInt64(-1), Dashboard: quota.Key("global_dashboard").MustInt64(-1),
ApiKey: quota.Key("global_api_key").MustInt64(-1), ApiKey: quota.Key("global_api_key").MustInt64(-1),
Session: quota.Key("global_session").MustInt64(-1), Session: quota.Key("global_session").MustInt64(-1),
AlertRule: alertGlobalQuota,
} }
cfg.Quota = Quota cfg.Quota = Quota

View File

@ -2,6 +2,7 @@ package alerting
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -10,6 +11,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/bus"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -26,15 +29,52 @@ import (
func TestAlertAndGroupsQuery(t *testing.T) { func TestAlertAndGroupsQuery(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"}, EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR, DisableAnonymous: true,
}) })
store := testinfra.SetUpDatabase(t, dir) store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// unauthenticated request to get the alerts should fail
{
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
require.JSONEq(t, `{"message": "Unauthorized"}`, string(b))
}
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
// invalid credentials request to get the alerts should fail
{
alertsURL := fmt.Sprintf("http://grafana:invalid@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
require.JSONEq(t, `{"error": "invalid username or password","message": "invalid username or password"}`, string(b))
}
// When there are no alerts available, it returns an empty list. // When there are no alerts available, it returns an empty list.
{ {
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr) alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Get(alertsURL) resp, err := http.Get(alertsURL)
require.NoError(t, err) require.NoError(t, err)
@ -50,7 +90,7 @@ func TestAlertAndGroupsQuery(t *testing.T) {
// When are there no alerts available, it returns an empty list of groups. // When are there no alerts available, it returns an empty list of groups.
{ {
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts/groups", grafanaListedAddr) alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts/groups", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Get(alertsURL) resp, err := http.Get(alertsURL)
require.NoError(t, err) require.NoError(t, err)
@ -106,7 +146,7 @@ func TestAlertAndGroupsQuery(t *testing.T) {
err = enc.Encode(&rules) err = enc.Encode(&rules)
require.NoError(t, err) require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", &buf) resp, err := http.Post(u, "application/json", &buf)
t.Cleanup(func() { t.Cleanup(func() {
@ -119,7 +159,7 @@ func TestAlertAndGroupsQuery(t *testing.T) {
// Eventually, we'll get an alert with its state being active. // Eventually, we'll get an alert with its state being active.
{ {
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr) alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec // nolint:gosec
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
resp, err := http.Get(alertsURL) resp, err := http.Get(alertsURL)
@ -150,11 +190,18 @@ func TestAlertRuleCRUD(t *testing.T) {
// Setup Grafana and its Database // Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"}, EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR, EnableQuota: true,
DisableAnonymous: true,
}) })
store := testinfra.SetUpDatabase(t, dir) store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
err := createUser(t, store, models.ROLE_EDITOR, "grafana", "password")
require.NoError(t, err)
// Create the namespace we'll save our alerts to. // Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default")) require.NoError(t, createFolder(t, store, 0, "default"))
@ -229,7 +276,7 @@ func TestAlertRuleCRUD(t *testing.T) {
Annotations: map[string]string{"annotation1": "val1"}, Annotations: map[string]string{"annotation1": "val1"},
}, },
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: getLongString(ngstore.AlertRuleMaxTitleLength + 1), Title: getLongString(t, ngstore.AlertRuleMaxTitleLength+1),
Condition: "A", Condition: "A",
Data: []ngmodels.AlertQuery{ Data: []ngmodels.AlertQuery{
{ {
@ -251,7 +298,7 @@ func TestAlertRuleCRUD(t *testing.T) {
}, },
{ {
desc: "alert rule with too long rulegroup", desc: "alert rule with too long rulegroup",
rulegroup: getLongString(ngstore.AlertRuleMaxTitleLength + 1), rulegroup: getLongString(t, ngstore.AlertRuleMaxTitleLength+1),
rule: apimodels.PostableExtendedRuleNode{ rule: apimodels.PostableExtendedRuleNode{
ApiRuleNode: &apimodels.ApiRuleNode{ ApiRuleNode: &apimodels.ApiRuleNode{
For: interval, For: interval,
@ -386,7 +433,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err := enc.Encode(&rules) err := enc.Encode(&rules)
require.NoError(t, err) require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", &buf) resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err) require.NoError(t, err)
@ -466,7 +513,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err := enc.Encode(&rules) err := enc.Encode(&rules)
require.NoError(t, err) require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", &buf) resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err) require.NoError(t, err)
@ -483,7 +530,7 @@ func TestAlertRuleCRUD(t *testing.T) {
// With the rules created, let's make sure that rule definition is stored correctly. // With the rules created, let's make sure that rule definition is stored correctly.
{ {
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Get(u) resp, err := http.Get(u)
require.NoError(t, err) require.NoError(t, err)
@ -523,7 +570,7 @@ func TestAlertRuleCRUD(t *testing.T) {
}, },
"grafana_alert":{ "grafana_alert":{
"id":1, "id":1,
"orgId":2, "orgId":1,
"title":"AlwaysFiring", "title":"AlwaysFiring",
"condition":"A", "condition":"A",
"data":[ "data":[
@ -558,7 +605,7 @@ func TestAlertRuleCRUD(t *testing.T) {
"expr":"", "expr":"",
"grafana_alert":{ "grafana_alert":{
"id":2, "id":2,
"orgId":2, "orgId":1,
"title":"AlwaysFiringButSilenced", "title":"AlwaysFiringButSilenced",
"condition":"A", "condition":"A",
"data":[ "data":[
@ -645,7 +692,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err = enc.Encode(&rules) err = enc.Encode(&rules)
require.NoError(t, err) require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", &buf) resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err) require.NoError(t, err)
@ -660,7 +707,7 @@ func TestAlertRuleCRUD(t *testing.T) {
require.JSONEq(t, `{"error":"failed to get alert rule unknown: could not find alert rule", "message": "failed to update rule group"}`, string(b)) require.JSONEq(t, `{"error":"failed to get alert rule unknown: could not find alert rule", "message": "failed to update rule group"}`, string(b))
// let's make sure that rule definitions are not affected by the failed POST request. // let's make sure that rule definitions are not affected by the failed POST request.
u = fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u = fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err = http.Get(u) resp, err = http.Get(u)
require.NoError(t, err) require.NoError(t, err)
@ -729,7 +776,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err = enc.Encode(&rules) err = enc.Encode(&rules)
require.NoError(t, err) require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", &buf) resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err) require.NoError(t, err)
@ -744,7 +791,7 @@ func TestAlertRuleCRUD(t *testing.T) {
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b)) require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
// let's make sure that rule definitions are updated correctly. // let's make sure that rule definitions are updated correctly.
u = fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u = fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err = http.Get(u) resp, err = http.Get(u)
require.NoError(t, err) require.NoError(t, err)
@ -782,7 +829,7 @@ func TestAlertRuleCRUD(t *testing.T) {
}, },
"grafana_alert":{ "grafana_alert":{
"id":1, "id":1,
"orgId":2, "orgId":1,
"title":"AlwaysNormal", "title":"AlwaysNormal",
"condition":"A", "condition":"A",
"data":[ "data":[
@ -823,7 +870,7 @@ func TestAlertRuleCRUD(t *testing.T) {
// Finally, make sure we can delete it. // Finally, make sure we can delete it.
{ {
t.Run("fail if he rule group name does not exists", func(t *testing.T) { t.Run("fail if he rule group name does not exists", func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default/groupnotexist", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default/groupnotexist", grafanaListedAddr)
req, err := http.NewRequest(http.MethodDelete, u, nil) req, err := http.NewRequest(http.MethodDelete, u, nil)
require.NoError(t, err) require.NoError(t, err)
resp, err := client.Do(req) resp, err := client.Do(req)
@ -840,7 +887,7 @@ func TestAlertRuleCRUD(t *testing.T) {
}) })
t.Run("succeed if the rule group name does exist", func(t *testing.T) { t.Run("succeed if the rule group name does exist", func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default/arulegroup", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default/arulegroup", grafanaListedAddr)
req, err := http.NewRequest(http.MethodDelete, u, nil) req, err := http.NewRequest(http.MethodDelete, u, nil)
require.NoError(t, err) require.NoError(t, err)
resp, err := client.Do(req) resp, err := client.Do(req)
@ -856,6 +903,124 @@ func TestAlertRuleCRUD(t *testing.T) {
require.JSONEq(t, `{"message":"rule group deleted"}`, string(b)) require.JSONEq(t, `{"message":"rule group deleted"}`, string(b))
}) })
} }
}
func TestQuota(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
EnableQuota: true,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default"))
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
// check quota limits
t.Run("when quota limit exceed", func(t *testing.T) {
// get existing org quota
query := models.GetOrgQuotaByTargetQuery{OrgId: 1, Target: "alert_rule"}
err = sqlstore.GetOrgQuotaByTarget(&query)
require.NoError(t, err)
used := query.Result.Used
limit := query.Result.Limit
// set org quota limit to equal used
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: 1,
Target: "alert_rule",
Limit: used,
}
err := sqlstore.UpdateOrgQuota(&orgCmd)
require.NoError(t, err)
t.Cleanup(func() {
// reset org quota to original value
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: 1,
Target: "alert_rule",
Limit: limit,
}
err := sqlstore.UpdateOrgQuota(&orgCmd)
require.NoError(t, err)
})
// try to create an alert rule
rules := apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Interval: interval,
Rules: []apimodels.PostableExtendedRuleNode{
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "One more alert rule",
Condition: "A",
Data: []ngmodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: ngmodels.RelativeTimeRange{
From: ngmodels.Duration(time.Duration(5) * time.Hour),
To: ngmodels.Duration(time.Duration(3) * time.Hour),
},
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
},
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
require.JSONEq(t, `{"message":"quota reached"}`, string(b))
})
}
func TestEval(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
EnableQuota: true,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
// Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default"))
// test eval conditions // test eval conditions
testCases := []struct { testCases := []struct {
@ -1022,7 +1187,7 @@ func TestAlertRuleCRUD(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/v1/rule/test/grafana", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/v1/rule/test/grafana", grafanaListedAddr)
r := strings.NewReader(tc.payload) r := strings.NewReader(tc.payload)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", r) resp, err := http.Post(u, "application/json", r)
@ -1178,7 +1343,7 @@ func TestAlertRuleCRUD(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/v1/eval", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/v1/eval", grafanaListedAddr)
r := strings.NewReader(tc.payload) r := strings.NewReader(tc.payload)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", r) resp, err := http.Post(u, "application/json", r)
@ -1202,7 +1367,7 @@ func createFolder(t *testing.T, store *sqlstore.SQLStore, folderID int64, folder
t.Helper() t.Helper()
cmd := models.SaveDashboardCommand{ cmd := models.SaveDashboardCommand{
OrgId: 2, // This is the orgID of the anonymous user. OrgId: 1, // default organisation
FolderId: folderID, FolderId: folderID,
IsFolder: true, IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{ Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -1244,7 +1409,21 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st
return string(json), m return string(json), m
} }
func getLongString(n int) string { func createUser(t *testing.T, store *sqlstore.SQLStore, role models.RoleType, username, password string) error {
t.Helper()
cmd := models.CreateUserCommand{
Login: username,
Password: password,
DefaultOrgRole: string(role),
}
_, err := store.CreateUser(context.Background(), cmd)
return err
}
func getLongString(t *testing.T, n int) string {
t.Helper()
b := make([]rune, n) b := make([]rune, n)
for i := range b { for i := range b {
b[i] = 'a' b[i] = 'a'

View File

@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -21,18 +22,23 @@ import (
func TestPrometheusRules(t *testing.T) { func TestPrometheusRules(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"}, EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR, DisableAnonymous: true,
}) })
store := testinfra.SetUpDatabase(t, dir) store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create the namespace we'll save our alerts to. // Create the namespace under default organisation (orgID = 1) where we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default")) require.NoError(t, createFolder(t, store, 0, "default"))
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
interval, err := model.ParseDuration("10s") interval, err := model.ParseDuration("10s")
require.NoError(t, err) require.NoError(t, err)
// When we have no alerting rules, it returns an empty list. // an unauthenticated request to get rules should fail
{ {
promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr) promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec // nolint:gosec
@ -42,6 +48,20 @@ func TestPrometheusRules(t *testing.T) {
err := resp.Body.Close() err := resp.Body.Close()
require.NoError(t, err) require.NoError(t, err)
}) })
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
// When we have no alerting rules, it returns an empty list.
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body) b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@ -109,7 +129,7 @@ func TestPrometheusRules(t *testing.T) {
err := enc.Encode(&rules) err := enc.Encode(&rules)
require.NoError(t, err) require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", &buf) resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err) require.NoError(t, err)
@ -126,7 +146,7 @@ func TestPrometheusRules(t *testing.T) {
// Now, let's see how this looks like. // Now, let's see how this looks like.
{ {
promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr) promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Get(promRulesURL) resp, err := http.Get(promRulesURL)
require.NoError(t, err) require.NoError(t, err)
@ -181,7 +201,7 @@ func TestPrometheusRules(t *testing.T) {
} }
{ {
promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr) promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec // nolint:gosec
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
resp, err := http.Get(promRulesURL) resp, err := http.Get(promRulesURL)

View File

@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -22,11 +23,16 @@ func TestAlertRulePermissions(t *testing.T) {
// Setup Grafana and its Database // Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"}, EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR, DisableAnonymous: true,
}) })
store := testinfra.SetUpDatabase(t, dir) store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
// Create the namespace we'll save our alerts to. // Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "folder1")) require.NoError(t, createFolder(t, store, 0, "folder1"))
@ -41,7 +47,7 @@ func TestAlertRulePermissions(t *testing.T) {
// With the rules created, let's make sure that rule definitions are stored. // With the rules created, let's make sure that rule definitions are stored.
{ {
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec // nolint:gosec
resp, err := http.Get(u) resp, err := http.Get(u)
require.NoError(t, err) require.NoError(t, err)
@ -73,7 +79,7 @@ func TestAlertRulePermissions(t *testing.T) {
}, },
"grafana_alert":{ "grafana_alert":{
"id":1, "id":1,
"orgId":2, "orgId":1,
"title":"rule under folder folder1", "title":"rule under folder folder1",
"condition":"A", "condition":"A",
"data":[ "data":[
@ -123,7 +129,7 @@ func TestAlertRulePermissions(t *testing.T) {
}, },
"grafana_alert":{ "grafana_alert":{
"id":2, "id":2,
"orgId":2, "orgId":1,
"title":"rule under folder folder2", "title":"rule under folder folder2",
"condition":"A", "condition":"A",
"data":[ "data":[
@ -195,7 +201,7 @@ func TestAlertRulePermissions(t *testing.T) {
}, },
"grafana_alert":{ "grafana_alert":{
"id":1, "id":1,
"orgId":2, "orgId":1,
"title":"rule under folder folder1", "title":"rule under folder folder1",
"condition":"A", "condition":"A",
"data":[ "data":[
@ -276,7 +282,7 @@ func createRule(t *testing.T, grafanaListedAddr string, folder string) {
err = enc.Encode(&rules) err = enc.Encode(&rules)
require.NoError(t, err) require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/%s", grafanaListedAddr, folder) u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/%s", grafanaListedAddr, folder)
// nolint:gosec // nolint:gosec
resp, err := http.Post(u, "application/json", &buf) resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err) require.NoError(t, err)

View File

@ -219,6 +219,18 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
_, err = anonSect.NewKey("org_role", string(o.AnonymousUserRole)) _, err = anonSect.NewKey("org_role", string(o.AnonymousUserRole))
require.NoError(t, err) require.NoError(t, err)
} }
if o.EnableQuota {
quotaSection, err := cfg.NewSection("quota")
require.NoError(t, err)
_, err = quotaSection.NewKey("enabled", "true")
require.NoError(t, err)
}
if o.DisableAnonymous {
anonSect, err := cfg.GetSection("auth.anonymous")
require.NoError(t, err)
_, err = anonSect.NewKey("enabled", "false")
require.NoError(t, err)
}
} }
cfgPath := filepath.Join(cfgDir, "test.ini") cfgPath := filepath.Join(cfgDir, "test.ini")
@ -235,4 +247,6 @@ type GrafanaOpts struct {
EnableCSP bool EnableCSP bool
EnableFeatureToggles []string EnableFeatureToggles []string
AnonymousUserRole models.RoleType AnonymousUserRole models.RoleType
EnableQuota bool
DisableAnonymous bool
} }