mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: introduce AlertRuleGroupKey and use it in API handlers (#48945)
* create AlertGroupKey structure * update PrometheusSrv. - extract creation of RuleGroup to a separate method. Use group key for grouping * update RuleSrv - update calculateChanges to use groupKey - authorize to use groupkey
This commit is contained in:
parent
85af8ce2ec
commit
952cb4fc0b
@ -157,28 +157,35 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
|
||||
return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator)
|
||||
}
|
||||
|
||||
groupMap := make(map[string]*apimodels.RuleGroup)
|
||||
|
||||
groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
|
||||
for _, rule := range alertRuleQuery.Result {
|
||||
if !authorizeDatasourceAccessForRule(rule, hasAccess) {
|
||||
continue
|
||||
}
|
||||
groupKey := rule.RuleGroup + "-" + rule.NamespaceUID
|
||||
newGroup, ok := groupMap[groupKey]
|
||||
if !ok {
|
||||
folder := namespaceMap[rule.NamespaceUID]
|
||||
if folder == nil {
|
||||
srv.log.Warn("query returned rules that belong to folder the user does not have access to. The rule will not be added to the response", "folder_uid", rule.NamespaceUID, "rule_uid", rule.UID)
|
||||
continue
|
||||
}
|
||||
newGroup = &apimodels.RuleGroup{
|
||||
Name: rule.RuleGroup,
|
||||
File: folder.Title, // file is what Prometheus uses for provisioning, we replace it with namespace.
|
||||
}
|
||||
groupMap[groupKey] = newGroup
|
||||
ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, newGroup)
|
||||
}
|
||||
key := rule.GetGroupKey()
|
||||
rulesInGroup := groupedRules[key]
|
||||
rulesInGroup = append(rulesInGroup, rule)
|
||||
groupedRules[key] = rulesInGroup
|
||||
}
|
||||
|
||||
for groupKey, rules := range groupedRules {
|
||||
folder := namespaceMap[groupKey.NamespaceUID]
|
||||
if folder == nil {
|
||||
srv.log.Warn("query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID)
|
||||
continue
|
||||
}
|
||||
ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, srv.toRuleGroup(groupKey.RuleGroup, folder, rules, labelOptions))
|
||||
}
|
||||
return response.JSON(http.StatusOK, ruleResponse)
|
||||
}
|
||||
|
||||
func (srv PrometheusSrv) toRuleGroup(groupName string, folder *models.Folder, rules []*ngmodels.AlertRule, labelOptions []ngmodels.LabelOption) *apimodels.RuleGroup {
|
||||
newGroup := &apimodels.RuleGroup{
|
||||
Name: groupName,
|
||||
File: folder.Title, // file is what Prometheus uses for provisioning, we replace it with namespace.
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
alertingRule := apimodels.AlertingRule{
|
||||
State: "inactive",
|
||||
Name: rule.Title,
|
||||
@ -195,7 +202,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
|
||||
LastEvaluation: time.Time{},
|
||||
}
|
||||
|
||||
for _, alertState := range srv.manager.GetStatesForRuleUID(c.OrgId, rule.UID) {
|
||||
for _, alertState := range srv.manager.GetStatesForRuleUID(rule.OrgID, rule.UID) {
|
||||
activeAt := alertState.StartsAt
|
||||
valString := ""
|
||||
if alertState.State == eval.Alerting || alertState.State == eval.Pending {
|
||||
@ -241,11 +248,11 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
|
||||
alertingRule.Rule = newRule
|
||||
newGroup.Rules = append(newGroup.Rules, alertingRule)
|
||||
newGroup.Interval = float64(rule.IntervalSeconds)
|
||||
// TODO yuri. Change that when scheduler will process alerts in groups
|
||||
newGroup.EvaluationTime = newRule.EvaluationTime
|
||||
newGroup.LastEvaluation = newRule.LastEvaluation
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, ruleResponse)
|
||||
return newGroup
|
||||
}
|
||||
|
||||
// ruleToQuery attempts to extract the datasource queries from the alert query model.
|
||||
|
@ -286,8 +286,6 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules")
|
||||
}
|
||||
|
||||
configs := make(map[string]map[string][]*ngmodels.AlertRule)
|
||||
|
||||
hasAccess := func(evaluator accesscontrol.Evaluator) bool {
|
||||
return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator)
|
||||
}
|
||||
@ -297,30 +295,25 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules")
|
||||
}
|
||||
|
||||
configs := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
|
||||
for _, r := range q.Result {
|
||||
if !authorizeDatasourceAccessForRule(r, hasAccess) {
|
||||
continue
|
||||
}
|
||||
namespaceCfgs, ok := configs[r.NamespaceUID]
|
||||
if !ok {
|
||||
namespaceCfgs = make(map[string][]*ngmodels.AlertRule)
|
||||
configs[r.NamespaceUID] = namespaceCfgs
|
||||
}
|
||||
group := namespaceCfgs[r.RuleGroup]
|
||||
groupKey := r.GetGroupKey()
|
||||
group := configs[groupKey]
|
||||
group = append(group, r)
|
||||
namespaceCfgs[r.RuleGroup] = group
|
||||
configs[groupKey] = group
|
||||
}
|
||||
|
||||
for namespaceUID, m := range configs {
|
||||
folder, ok := namespaceMap[namespaceUID]
|
||||
for groupKey, rules := range configs {
|
||||
folder, ok := namespaceMap[groupKey.NamespaceUID]
|
||||
if !ok {
|
||||
srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", namespaceUID)
|
||||
srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", groupKey.NamespaceUID)
|
||||
continue
|
||||
}
|
||||
namespace := folder.Title
|
||||
for groupName, groupRules := range m {
|
||||
result[namespace] = append(result[namespace], toGettableRuleGroupConfig(groupName, groupRules, folder.Id, provenanceRecords))
|
||||
}
|
||||
result[namespace] = append(result[namespace], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, folder.Id, provenanceRecords))
|
||||
}
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
@ -337,19 +330,24 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
|
||||
return srv.updateAlertRulesInGroup(c, namespace, ruleGroupConfig.Name, rules)
|
||||
groupKey := ngmodels.AlertRuleGroupKey{
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
NamespaceUID: namespace.Uid,
|
||||
RuleGroup: ruleGroupConfig.Name,
|
||||
}
|
||||
|
||||
return srv.updateAlertRulesInGroup(c, groupKey, rules)
|
||||
}
|
||||
|
||||
// updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database.
|
||||
// All operations are performed in a single transaction
|
||||
//nolint: gocyclo
|
||||
func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *models.Folder, groupName string, rules []*ngmodels.AlertRule) response.Response {
|
||||
// nolint: gocyclo
|
||||
func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmodels.AlertRuleGroupKey, rules []*ngmodels.AlertRule) response.Response {
|
||||
var finalChanges *changes
|
||||
hasAccess := accesscontrol.HasAccess(srv.ac, c)
|
||||
err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error {
|
||||
logger := srv.log.New("namespace_uid", namespace.Uid, "group", groupName, "org_id", c.OrgId, "user_id", c.UserId)
|
||||
|
||||
groupChanges, err := calculateChanges(tranCtx, srv.store, c.SignedInUser.OrgId, namespace, groupName, rules)
|
||||
logger := srv.log.New("namespace_uid", groupKey.NamespaceUID, "group", groupKey.RuleGroup, "org_id", groupKey.OrgID, "user_id", c.UserId)
|
||||
groupChanges, err := calculateChanges(tranCtx, srv.store, groupKey, rules)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -360,7 +358,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
|
||||
return nil
|
||||
}
|
||||
|
||||
authorizedChanges, err := authorizeRuleChanges(namespace, groupChanges, func(evaluator accesscontrol.Evaluator) bool {
|
||||
authorizedChanges, err := authorizeRuleChanges(groupChanges, func(evaluator accesscontrol.Evaluator) bool {
|
||||
return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator)
|
||||
})
|
||||
if err != nil {
|
||||
@ -565,9 +563,10 @@ type ruleUpdate struct {
|
||||
}
|
||||
|
||||
type changes struct {
|
||||
New []*ngmodels.AlertRule
|
||||
Update []ruleUpdate
|
||||
Delete []*ngmodels.AlertRule
|
||||
GroupKey ngmodels.AlertRuleGroupKey
|
||||
New []*ngmodels.AlertRule
|
||||
Update []ruleUpdate
|
||||
Delete []*ngmodels.AlertRule
|
||||
}
|
||||
|
||||
func (c *changes) isEmpty() bool {
|
||||
@ -576,14 +575,14 @@ func (c *changes) isEmpty() bool {
|
||||
|
||||
// calculateChanges calculates the difference between rules in the group in the database and the submitted rules. If a submitted rule has UID it tries to find it in the database (in other groups).
|
||||
// returns a list of rules that need to be added, updated and deleted. Deleted considered rules in the database that belong to the group but do not exist in the list of submitted rules.
|
||||
func calculateChanges(ctx context.Context, ruleStore store.RuleStore, orgId int64, namespace *models.Folder, ruleGroupName string, submittedRules []*ngmodels.AlertRule) (*changes, error) {
|
||||
func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey ngmodels.AlertRuleGroupKey, submittedRules []*ngmodels.AlertRule) (*changes, error) {
|
||||
q := &ngmodels.ListAlertRulesQuery{
|
||||
OrgID: orgId,
|
||||
NamespaceUIDs: []string{namespace.Uid},
|
||||
RuleGroup: ruleGroupName,
|
||||
OrgID: groupKey.OrgID,
|
||||
NamespaceUIDs: []string{groupKey.NamespaceUID},
|
||||
RuleGroup: groupKey.RuleGroup,
|
||||
}
|
||||
if err := ruleStore.ListAlertRules(ctx, q); err != nil {
|
||||
return nil, fmt.Errorf("failed to query database for rules in the group %s: %w", ruleGroupName, err)
|
||||
return nil, fmt.Errorf("failed to query database for rules in the group %s: %w", groupKey, err)
|
||||
}
|
||||
existingGroupRules := q.Result
|
||||
|
||||
@ -604,7 +603,7 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, orgId int6
|
||||
delete(existingGroupRulesUIDs, r.UID)
|
||||
} else {
|
||||
// Rule can be from other group or namespace
|
||||
q := &ngmodels.GetAlertRuleByUIDQuery{OrgID: orgId, UID: r.UID}
|
||||
q := &ngmodels.GetAlertRuleByUIDQuery{OrgID: groupKey.OrgID, UID: r.UID}
|
||||
if err := ruleStore.GetAlertRuleByUID(ctx, q); err != nil || q.Result == nil {
|
||||
// if rule has UID then it is considered an update. Therefore, fail if there is no rule to update
|
||||
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) || q.Result == nil && err == nil {
|
||||
@ -641,9 +640,10 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, orgId int6
|
||||
}
|
||||
|
||||
return &changes{
|
||||
New: toAdd,
|
||||
Delete: toDelete,
|
||||
Update: toUpdate,
|
||||
GroupKey: groupKey,
|
||||
New: toAdd,
|
||||
Delete: toDelete,
|
||||
Update: toUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -33,11 +33,10 @@ func TestCalculateChanges(t *testing.T) {
|
||||
t.Run("detects alerts that need to be added", func(t *testing.T) {
|
||||
fakeStore := store.NewFakeRuleStore(t)
|
||||
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
submitted := models.GenerateAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), simulateSubmitted, withoutUID))
|
||||
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, changes.New, len(submitted))
|
||||
@ -56,14 +55,13 @@ func TestCalculateChanges(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("detects alerts that need to be deleted", func(t *testing.T) {
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), withGroup(groupName), withNamespace(namespace)))
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
|
||||
fakeStore := store.NewFakeRuleStore(t)
|
||||
fakeStore.PutRule(context.Background(), inDatabase...)
|
||||
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, make([]*models.AlertRule, 0))
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, make([]*models.AlertRule, 0))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, changes.New)
|
||||
@ -77,15 +75,14 @@ func TestCalculateChanges(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should detect alerts that needs to be updated", func(t *testing.T) {
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), withGroup(groupName), withNamespace(namespace)))
|
||||
submittedMap, submitted := models.GenerateUniqueAlertRules(len(inDatabase), models.AlertRuleGen(simulateSubmitted, withOrgID(orgId), withGroup(groupName), withNamespace(namespace), withUIDs(inDatabaseMap)))
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
submittedMap, submitted := models.GenerateUniqueAlertRules(len(inDatabase), models.AlertRuleGen(simulateSubmitted, withGroupKey(groupKey), withUIDs(inDatabaseMap)))
|
||||
|
||||
fakeStore := store.NewFakeRuleStore(t)
|
||||
fakeStore.PutRule(context.Background(), inDatabase...)
|
||||
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, changes.Update, len(inDatabase))
|
||||
@ -101,9 +98,8 @@ func TestCalculateChanges(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should include only if there are changes ignoring specific fields", func(t *testing.T) {
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
_, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), withGroup(groupName), withNamespace(namespace)))
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
_, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
|
||||
submitted := make([]*models.AlertRule, 0, len(inDatabase))
|
||||
for _, rule := range inDatabase {
|
||||
@ -120,7 +116,7 @@ func TestCalculateChanges(t *testing.T) {
|
||||
fakeStore := store.NewFakeRuleStore(t)
|
||||
fakeStore.PutRule(context.Background(), inDatabase...)
|
||||
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, changes.Update)
|
||||
@ -171,15 +167,14 @@ func TestCalculateChanges(t *testing.T) {
|
||||
fakeStore := store.NewFakeRuleStore(t)
|
||||
fakeStore.PutRule(context.Background(), dbRule)
|
||||
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
expected := models.AlertRuleGen(simulateSubmitted, testCase.mutator)()
|
||||
expected.UID = dbRule.UID
|
||||
submitted := *expected
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{&submitted})
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRule{&submitted})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, changes.Update, 1)
|
||||
ch := changes.Update[0]
|
||||
@ -199,9 +194,16 @@ func TestCalculateChanges(t *testing.T) {
|
||||
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
submittedMap, submitted := models.GenerateUniqueAlertRules(rand.Intn(len(inDatabase)-5)+5, models.AlertRuleGen(simulateSubmitted, withOrgID(orgId), withGroup(groupName), withNamespace(namespace), withUIDs(inDatabaseMap)))
|
||||
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
|
||||
groupKey := models.AlertRuleGroupKey{
|
||||
OrgID: orgId,
|
||||
NamespaceUID: namespace.Uid,
|
||||
RuleGroup: groupName,
|
||||
}
|
||||
|
||||
submittedMap, submitted := models.GenerateUniqueAlertRules(rand.Intn(len(inDatabase)-5)+5, models.AlertRuleGen(simulateSubmitted, withGroupKey(groupKey), withUIDs(inDatabaseMap)))
|
||||
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, changes.Delete)
|
||||
@ -218,13 +220,11 @@ func TestCalculateChanges(t *testing.T) {
|
||||
|
||||
t.Run("should fail when submitted rule has UID that does not exist in db", func(t *testing.T) {
|
||||
fakeStore := store.NewFakeRuleStore(t)
|
||||
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)()
|
||||
require.NotEqual(t, "", submitted.UID)
|
||||
|
||||
_, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{submitted})
|
||||
_, err := calculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRule{submitted})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
@ -239,11 +239,10 @@ func TestCalculateChanges(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted, withoutUID)()
|
||||
|
||||
_, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{submitted})
|
||||
_, err := calculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRule{submitted})
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
})
|
||||
|
||||
@ -258,11 +257,10 @@ func TestCalculateChanges(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
namespace := randFolder()
|
||||
groupName := util.GenerateShortUID()
|
||||
groupKey := models.GenerateGroupKey(orgId)
|
||||
submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)()
|
||||
|
||||
_, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{submitted})
|
||||
_, err := calculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRule{submitted})
|
||||
require.Error(t, err, expectedErr)
|
||||
})
|
||||
}
|
||||
@ -705,6 +703,14 @@ func withNamespace(namespace *models2.Folder) func(rule *models.AlertRule) {
|
||||
}
|
||||
}
|
||||
|
||||
func withGroupKey(groupKey models.AlertRuleGroupKey) func(rule *models.AlertRule) {
|
||||
return func(rule *models.AlertRule) {
|
||||
rule.RuleGroup = groupKey.RuleGroup
|
||||
rule.OrgID = groupKey.OrgID
|
||||
rule.NamespaceUID = groupKey.NamespaceUID
|
||||
}
|
||||
}
|
||||
|
||||
// simulateSubmitted resets some fields of the structure that are not populated by API model to model conversion
|
||||
func simulateSubmitted(rule *models.AlertRule) {
|
||||
rule.ID = 0
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
@ -218,14 +217,15 @@ func authorizeDatasourceAccessForRule(rule *ngmodels.AlertRule, evaluator func(e
|
||||
// NOTE: if there are rules for deletion, and the user does not have access to data sources that a rule uses, the rule is removed from the list.
|
||||
// If the user is not authorized to perform the changes the function returns ErrAuthorization with a description of what action is not authorized.
|
||||
// Return changes that the user is authorized to perform or ErrAuthorization
|
||||
func authorizeRuleChanges(namespace *models.Folder, change *changes, evaluator func(evaluator ac.Evaluator) bool) (*changes, error) {
|
||||
func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator) bool) (*changes, error) {
|
||||
var result = &changes{
|
||||
New: change.New,
|
||||
Update: change.Update,
|
||||
Delete: change.Delete,
|
||||
GroupKey: change.GroupKey,
|
||||
New: change.New,
|
||||
Update: change.Update,
|
||||
Delete: change.Delete,
|
||||
}
|
||||
|
||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(namespace.Uid)
|
||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
|
||||
if len(change.Delete) > 0 {
|
||||
var allowedToDelete []*ngmodels.AlertRule
|
||||
for _, rule := range change.Delete {
|
||||
@ -237,7 +237,7 @@ func authorizeRuleChanges(namespace *models.Folder, change *changes, evaluator f
|
||||
if len(allowedToDelete) > 0 {
|
||||
allowed := evaluator(ac.EvalPermission(ac.ActionAlertingRuleDelete, namespaceScope))
|
||||
if !allowed {
|
||||
return nil, fmt.Errorf("%w to delete alert rules that belong to folder %s", ErrAuthorization, namespace.Title)
|
||||
return nil, fmt.Errorf("%w to delete alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
|
||||
}
|
||||
}
|
||||
result.Delete = allowedToDelete
|
||||
@ -248,7 +248,7 @@ func authorizeRuleChanges(namespace *models.Folder, change *changes, evaluator f
|
||||
if len(change.New) > 0 {
|
||||
addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope))
|
||||
if !addAuthorized {
|
||||
return nil, fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, namespace.Title)
|
||||
return nil, fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
|
||||
}
|
||||
for _, rule := range change.New {
|
||||
dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator)
|
||||
@ -274,7 +274,7 @@ func authorizeRuleChanges(namespace *models.Folder, change *changes, evaluator f
|
||||
if !addAuthorized {
|
||||
addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope))
|
||||
if !addAuthorized {
|
||||
return nil, fmt.Errorf("%w to create alert rules in the folder '%s'", ErrAuthorization, namespace.Title)
|
||||
return nil, fmt.Errorf("%w to create alert rules in the folder '%s'", ErrAuthorization, change.GroupKey.NamespaceUID)
|
||||
}
|
||||
}
|
||||
continue
|
||||
@ -283,7 +283,7 @@ func authorizeRuleChanges(namespace *models.Folder, change *changes, evaluator f
|
||||
if !updateAuthorized { // if it is false then the authorization was not checked. If it is true then the user is authorized to update rules
|
||||
updateAuthorized = evaluator(ac.EvalAll(ac.EvalPermission(ac.ActionAlertingRuleUpdate, namespaceScope)))
|
||||
if !updateAuthorized {
|
||||
return nil, fmt.Errorf("%w to update alert rules that belong to folder %s", ErrAuthorization, namespace.Title)
|
||||
return nil, fmt.Errorf("%w to update alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,8 +69,8 @@ func TestAuthorize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
namespace := randFolder()
|
||||
namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(namespace.Uid)
|
||||
groupKey := models.GenerateGroupKey(rand.Int63())
|
||||
namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@ -81,9 +81,10 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
name: "if there are rules to add it should check create action and query for datasource",
|
||||
changes: func() *changes {
|
||||
return &changes{
|
||||
New: models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withNamespace(namespace))),
|
||||
Update: nil,
|
||||
Delete: nil,
|
||||
GroupKey: groupKey,
|
||||
New: models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey))),
|
||||
Update: nil,
|
||||
Delete: nil,
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -104,7 +105,7 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
{
|
||||
name: "if there are rules to update within the same namespace it should check update action",
|
||||
changes: func() *changes {
|
||||
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withNamespace(namespace)))
|
||||
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
updates := make([]ruleUpdate, 0, len(rules))
|
||||
|
||||
for _, rule := range rules {
|
||||
@ -116,9 +117,10 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
return &changes{
|
||||
New: nil,
|
||||
Update: updates,
|
||||
Delete: nil,
|
||||
GroupKey: groupKey,
|
||||
New: nil,
|
||||
Update: updates,
|
||||
Delete: nil,
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -140,7 +142,7 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
{
|
||||
name: "if there are rules that are moved between namespaces it should check update action",
|
||||
changes: func() *changes {
|
||||
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withNamespace(namespace)))
|
||||
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
updates := make([]ruleUpdate, 0, len(rules))
|
||||
|
||||
for _, rule := range rules {
|
||||
@ -154,9 +156,10 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
return &changes{
|
||||
New: nil,
|
||||
Update: updates,
|
||||
Delete: nil,
|
||||
GroupKey: groupKey,
|
||||
New: nil,
|
||||
Update: updates,
|
||||
Delete: nil,
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -168,7 +171,7 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
}
|
||||
return map[string][]string{
|
||||
ac.ActionAlertingRuleDelete: {
|
||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(namespace.Uid + "other"),
|
||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID + "other"),
|
||||
},
|
||||
ac.ActionAlertingRuleCreate: {
|
||||
namespaceIdScope,
|
||||
@ -185,7 +188,7 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
|
||||
groupChanges := testCase.changes()
|
||||
|
||||
result, err := authorizeRuleChanges(namespace, groupChanges, func(evaluator ac.Evaluator) bool {
|
||||
result, err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
|
||||
response, err := evaluator.Evaluate(make(map[string][]string))
|
||||
require.False(t, response)
|
||||
require.NoError(t, err)
|
||||
@ -198,7 +201,7 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
|
||||
permissions := testCase.permissions(groupChanges)
|
||||
executed = false
|
||||
result, err = authorizeRuleChanges(namespace, groupChanges, func(evaluator ac.Evaluator) bool {
|
||||
result, err = authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
|
||||
response, err := evaluator.Evaluate(permissions)
|
||||
require.Truef(t, response, "provided permissions [%v] is not enough for requested permissions [%s]", testCase.permissions, evaluator.GoString())
|
||||
require.NoError(t, err)
|
||||
@ -213,8 +216,8 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
namespace := randFolder()
|
||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(namespace.Uid)
|
||||
groupKey := models.GenerateGroupKey(rand.Int63())
|
||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
|
||||
|
||||
getScopes := func(rules []*models.AlertRule) []string {
|
||||
var scopes []string
|
||||
@ -236,9 +239,10 @@ func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
name: "should validate check access to data source and folder",
|
||||
changes: func() *changes {
|
||||
return &changes{
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withNamespace(namespace))),
|
||||
GroupKey: groupKey,
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -258,9 +262,10 @@ func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
name: "should remove rules user does not have access to data source",
|
||||
changes: func() *changes {
|
||||
return &changes{
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withNamespace(namespace))),
|
||||
GroupKey: groupKey,
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -282,9 +287,10 @@ func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
name: "should not fail if no changes other than unauthorized",
|
||||
changes: func() *changes {
|
||||
return &changes{
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withNamespace(namespace))),
|
||||
GroupKey: groupKey,
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -304,9 +310,10 @@ func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
name: "should not fail if there are changes and no rules can be deleted",
|
||||
changes: func() *changes {
|
||||
return &changes{
|
||||
New: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withNamespace(namespace))),
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withNamespace(namespace))),
|
||||
GroupKey: groupKey,
|
||||
New: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -329,9 +336,10 @@ func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
name: "should fail if no access to folder",
|
||||
changes: func() *changes {
|
||||
return &changes{
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withNamespace(namespace))),
|
||||
GroupKey: groupKey,
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
@ -350,7 +358,7 @@ func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
groupChanges := testCase.changes()
|
||||
permissions := testCase.permissions(groupChanges)
|
||||
result, err := authorizeRuleChanges(namespace, groupChanges, func(evaluator ac.Evaluator) bool {
|
||||
result, err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
|
||||
response, err := evaluator.Evaluate(permissions)
|
||||
require.NoError(t, err)
|
||||
return response
|
||||
|
@ -167,6 +167,17 @@ type AlertRuleKey struct {
|
||||
UID string
|
||||
}
|
||||
|
||||
// AlertRuleGroupKey is the identifier of a group of alerts
|
||||
type AlertRuleGroupKey struct {
|
||||
OrgID int64
|
||||
NamespaceUID string
|
||||
RuleGroup string
|
||||
}
|
||||
|
||||
func (k AlertRuleGroupKey) String() string {
|
||||
return fmt.Sprintf("{orgID: %d, namespaceUID: %s, groupName: %s}", k.OrgID, k.NamespaceUID, k.RuleGroup)
|
||||
}
|
||||
|
||||
func (k AlertRuleKey) String() string {
|
||||
return fmt.Sprintf("{orgID: %d, UID: %s}", k.OrgID, k.UID)
|
||||
}
|
||||
@ -176,6 +187,11 @@ func (alertRule *AlertRule) GetKey() AlertRuleKey {
|
||||
return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID}
|
||||
}
|
||||
|
||||
// GetGroupKey returns the identifier of a group the rule belongs to
|
||||
func (alertRule *AlertRule) GetGroupKey() AlertRuleGroupKey {
|
||||
return AlertRuleGroupKey{OrgID: alertRule.OrgID, NamespaceUID: alertRule.NamespaceUID, RuleGroup: alertRule.RuleGroup}
|
||||
}
|
||||
|
||||
// GetKey returns the alert definitions identifier
|
||||
func (alertRule *SchedulableAlertRule) GetKey() AlertRuleKey {
|
||||
return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID}
|
||||
|
@ -133,6 +133,15 @@ func GenerateAlertRules(count int, f func() *AlertRule) []*AlertRule {
|
||||
return result
|
||||
}
|
||||
|
||||
// GenerateGroupKey generates many random alert rules. Does not guarantee that rules are unique (by UID)
|
||||
func GenerateGroupKey(orgID int64) AlertRuleGroupKey {
|
||||
return AlertRuleGroupKey{
|
||||
OrgID: orgID,
|
||||
NamespaceUID: util.GenerateShortUID(),
|
||||
RuleGroup: util.GenerateShortUID(),
|
||||
}
|
||||
}
|
||||
|
||||
// CopyRule creates a deep copy of AlertRule
|
||||
func CopyRule(r *AlertRule) *AlertRule {
|
||||
result := AlertRule{
|
||||
|
Loading…
Reference in New Issue
Block a user