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:
Yuriy Tseretyan 2022-05-16 15:45:45 -04:00 committed by GitHub
parent 85af8ce2ec
commit 952cb4fc0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 175 additions and 129 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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}

View File

@ -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{